unrealircd

- supernets unrealircd source & configuration
git clone git://git.acid.vegas/unrealircd.git
Log | Files | Refs | Archive | README | LICENSE

authprompt.c (14241B)

      1 /*
      2  * Auth prompt: SASL authentication for clients that don't support SASL
      3  * (C) Copyright 2018 Bram Matthys ("Syzop") and the UnrealIRCd team
      4  *
      5  * This program is free software; you can redistribute it and/or modify
      6  * it under the terms of the GNU General Public License as published by
      7  * the Free Software Foundation; either version 1, or (at your option)
      8  * any later version.
      9  *
     10  * This program is distributed in the hope that it will be useful,
     11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
     12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     13  * GNU General Public License for more details.
     14  *
     15  * You should have received a copy of the GNU General Public License
     16  * along with this program; if not, write to the Free Software
     17  * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
     18  */
     19 
     20 #include "unrealircd.h"
     21 
     22 ModuleHeader MOD_HEADER
     23 = {
     24 	"authprompt",
     25 	"1.0",
     26 	"SASL authentication for clients that don't support SASL",
     27 	"UnrealIRCd Team",
     28 	"unrealircd-6",
     29 };
     30 
     31 /** Configuration settings */
     32 struct {
     33 	int enabled;
     34 	MultiLine *message;
     35 	MultiLine *fail_message;
     36 	MultiLine *unconfirmed_message;
     37 } cfg;
     38 
     39 /** User struct */
     40 typedef struct APUser APUser;
     41 struct APUser {
     42 	char *authmsg;
     43 	char *reason;
     44 };
     45 
     46 /* Global variables */
     47 ModDataInfo *authprompt_md = NULL;
     48 
     49 /* Forward declarations */
     50 static void free_config(void);
     51 static void init_config(void);
     52 static void config_postdefaults(void);
     53 int authprompt_config_test(ConfigFile *, ConfigEntry *, int, int *);
     54 int authprompt_config_run(ConfigFile *, ConfigEntry *, int);
     55 int authprompt_sasl_continuation(Client *client, const char *buf);
     56 int authprompt_sasl_result(Client *client, int success);
     57 int authprompt_place_host_ban(Client *client, int action, const char *reason, long duration);
     58 int authprompt_find_tkline_match(Client *client, TKL *tk);
     59 int authprompt_pre_local_handshake_timeout(Client *client, const char **comment);
     60 int authprompt_pre_connect(Client *client);
     61 CMD_FUNC(cmd_auth);
     62 void authprompt_md_free(ModData *md);
     63 
     64 /* Some macros */
     65 #define SetAPUser(x, y) do { moddata_client(x, authprompt_md).ptr = y; } while(0)
     66 #define SEUSER(x)       ((APUser *)moddata_client(x, authprompt_md).ptr)
     67 #define AGENT_SID(agent_p)      (agent_p->user != NULL ? agent_p->user->server : agent_p->name)
     68 
     69 MOD_TEST()
     70 {
     71 	HookAdd(modinfo->handle, HOOKTYPE_CONFIGTEST, 0, authprompt_config_test);
     72 	return MOD_SUCCESS;
     73 }
     74 
     75 MOD_INIT()
     76 {
     77 	ModDataInfo mreq;
     78 
     79 	MARK_AS_OFFICIAL_MODULE(modinfo);
     80 
     81 	memset(&mreq, 0, sizeof(mreq));
     82 	mreq.name = "authprompt";
     83 	mreq.type = MODDATATYPE_CLIENT;
     84 	mreq.free = authprompt_md_free;
     85 	authprompt_md = ModDataAdd(modinfo->handle, mreq);
     86 	if (!authprompt_md)
     87 	{
     88 		config_error("could not register authprompt moddata");
     89 		return MOD_FAILED;
     90 	}
     91 
     92 	init_config();
     93 	HookAdd(modinfo->handle, HOOKTYPE_CONFIGRUN, 0, authprompt_config_run);
     94 	HookAdd(modinfo->handle, HOOKTYPE_SASL_CONTINUATION, 0, authprompt_sasl_continuation);
     95 	HookAdd(modinfo->handle, HOOKTYPE_SASL_RESULT, 0, authprompt_sasl_result);
     96 	HookAdd(modinfo->handle, HOOKTYPE_PLACE_HOST_BAN, 0, authprompt_place_host_ban);
     97 	HookAdd(modinfo->handle, HOOKTYPE_FIND_TKLINE_MATCH, 0, authprompt_find_tkline_match);
     98 	HookAdd(modinfo->handle, HOOKTYPE_PRE_LOCAL_HANDSHAKE_TIMEOUT, 0, authprompt_pre_local_handshake_timeout);
     99 	/* For HOOKTYPE_PRE_LOCAL_CONNECT we want a low priority, so we are called last.
    100 	 * This gives hooks like the one from the blacklist module (pending softban)
    101 	 * a chance to be handled first.
    102 	 */
    103 	HookAdd(modinfo->handle, HOOKTYPE_PRE_LOCAL_CONNECT, -1000000, authprompt_pre_connect);
    104 	CommandAdd(modinfo->handle, "AUTH", cmd_auth, 1, CMD_UNREGISTERED);
    105 	return MOD_SUCCESS;
    106 }
    107 
    108 MOD_LOAD()
    109 {
    110 	config_postdefaults();
    111 	return MOD_SUCCESS;
    112 }
    113 
    114 MOD_UNLOAD()
    115 {
    116 	free_config();
    117 	return MOD_SUCCESS;
    118 }
    119 
    120 static void init_config(void)
    121 {
    122 	/* This sets some default values */
    123 	memset(&cfg, 0, sizeof(cfg));
    124 	cfg.enabled = 1;
    125 }
    126 
    127 static void config_postdefaults(void)
    128 {
    129 	if (!cfg.message)
    130 	{
    131 		addmultiline(&cfg.message, "The server requires clients from this IP address to authenticate with a registered nickname and password.");
    132 		addmultiline(&cfg.message, "Please reconnect using SASL, or authenticate now by typing: /QUOTE AUTH nick:password");
    133 	}
    134 	if (!cfg.fail_message)
    135 	{
    136 		addmultiline(&cfg.fail_message, "Authentication failed.");
    137 	}
    138 	if (!cfg.unconfirmed_message)
    139 	{
    140 		addmultiline(&cfg.unconfirmed_message, "You are trying to use an unconfirmed services account.");
    141 		addmultiline(&cfg.unconfirmed_message, "This services account can only be used after it has been activated/confirmed.");
    142 	}
    143 }
    144 
    145 static void free_config(void)
    146 {
    147 	freemultiline(cfg.message);
    148 	freemultiline(cfg.fail_message);
    149 	freemultiline(cfg.unconfirmed_message);
    150 	memset(&cfg, 0, sizeof(cfg)); /* needed! */
    151 }
    152 
    153 int authprompt_config_test(ConfigFile *cf, ConfigEntry *ce, int type, int *errs)
    154 {
    155 	int errors = 0;
    156 	ConfigEntry *cep;
    157 
    158 	if (type != CONFIG_SET)
    159 		return 0;
    160 
    161 	/* We are only interrested in set::authentication-prompt... */
    162 	if (!ce || !ce->name || strcmp(ce->name, "authentication-prompt"))
    163 		return 0;
    164 
    165 	for (cep = ce->items; cep; cep = cep->next)
    166 	{
    167 		if (!cep->value)
    168 		{
    169 			config_error("%s:%i: set::authentication-prompt::%s with no value",
    170 				cep->file->filename, cep->line_number, cep->name);
    171 			errors++;
    172 		} else
    173 		if (!strcmp(cep->name, "enabled"))
    174 		{
    175 		} else
    176 		if (!strcmp(cep->name, "message"))
    177 		{
    178 		} else
    179 		if (!strcmp(cep->name, "fail-message"))
    180 		{
    181 		} else
    182 		if (!strcmp(cep->name, "unconfirmed-message"))
    183 		{
    184 		} else
    185 		{
    186 			config_error("%s:%i: unknown directive set::authentication-prompt::%s",
    187 				cep->file->filename, cep->line_number, cep->name);
    188 			errors++;
    189 		}
    190 	}
    191 	*errs = errors;
    192 	return errors ? -1 : 1;
    193 }
    194 
    195 int authprompt_config_run(ConfigFile *cf, ConfigEntry *ce, int type)
    196 {
    197 	ConfigEntry *cep;
    198 
    199 	if (type != CONFIG_SET)
    200 		return 0;
    201 
    202 	/* We are only interrested in set::authentication-prompt... */
    203 	if (!ce || !ce->name || strcmp(ce->name, "authentication-prompt"))
    204 		return 0;
    205 
    206 	for (cep = ce->items; cep; cep = cep->next)
    207 	{
    208 		if (!strcmp(cep->name, "enabled"))
    209 		{
    210 			cfg.enabled = config_checkval(cep->value, CFG_YESNO);
    211 		} else
    212 		if (!strcmp(cep->name, "message"))
    213 		{
    214 			addmultiline(&cfg.message, cep->value);
    215 		} else
    216 		if (!strcmp(cep->name, "fail-message"))
    217 		{
    218 			addmultiline(&cfg.fail_message, cep->value);
    219 		} else
    220 		if (!strcmp(cep->name, "unconfirmed-message"))
    221 		{
    222 			addmultiline(&cfg.unconfirmed_message, cep->value);
    223 		}
    224 	}
    225 	return 1;
    226 }
    227 
    228 void authprompt_md_free(ModData *md)
    229 {
    230 	APUser *se = md->ptr;
    231 
    232 	if (se)
    233 	{
    234 		safe_free(se->authmsg);
    235 		safe_free(se->reason);
    236 		safe_free(se);
    237 		md->ptr = se = NULL;
    238 	}
    239 }
    240 
    241 /** Parse an authentication request from the user (form: <user>:<pass>).
    242  * @param str      The input string with the request.
    243  * @param username Pointer to the username string.
    244  * @param password Pointer to the password string.
    245  * @retval 1 if the format is correct, 0 if not.
    246  * @note The returned 'username' and 'password' are valid until next call to parse_nickpass().
    247  */
    248 int parse_nickpass(const char *str, char **username, char **password)
    249 {
    250 	static char buf[250];
    251 	char *p;
    252 
    253 	strlcpy(buf, str, sizeof(buf));
    254 
    255 	p = strchr(buf, ':');
    256 	if (!p)
    257 		return 0;
    258 
    259 	*p++ = '\0';
    260 	*username = buf;
    261 	*password = p;
    262 
    263 	if (!*username[0] || !*password[0])
    264 		return 0;
    265 
    266 	return 1;
    267 }
    268 
    269 char *make_authbuf(const char *username, const char *password)
    270 {
    271 	char inbuf[256];
    272 	static char outbuf[512];
    273 	int size;
    274 
    275 	size = strlen(username) + 1 + strlen(username) + 1 + strlen(password);
    276 	if (size >= sizeof(inbuf)-1)
    277 		return NULL; /* too long */
    278 
    279 	/* Because size limits are already checked above, we can cut some corners here: */
    280 	memset(inbuf, 0, sizeof(inbuf));
    281 	strcpy(inbuf, username);
    282 	strcpy(inbuf+strlen(username)+1, username);
    283 	strcpy(inbuf+strlen(username)+1+strlen(username)+1, password);
    284 	/* ^ normal people use stpcpy here ;) */
    285 
    286 	if (b64_encode(inbuf, size, outbuf, sizeof(outbuf)) < 0)
    287 		return NULL; /* base64 encoding error */
    288 
    289 	return outbuf;
    290 }
    291 
    292 /** Send first SASL authentication request (AUTHENTICATE PLAIN).
    293  * Among other things, this is used to discover the agent
    294  * which will later be used for this session.
    295  */
    296 void send_first_auth(Client *client)
    297 {
    298 	Client *sasl_server;
    299 	char *addr = BadPtr(client->ip) ? "0" : client->ip;
    300 	const char *certfp = moddata_client_get(client, "certfp");
    301 
    302 	sasl_server = find_client(SASL_SERVER, NULL);
    303 	if (!sasl_server)
    304 	{
    305 		/* Services down. */
    306 		return;
    307 	}
    308 
    309 	/* Make them a user, needed for CHGHOST etc that we may receive */
    310 	if (!client->user)
    311 		make_user(client);
    312 
    313 	sendto_one(sasl_server, NULL, ":%s SASL %s %s H %s %s",
    314 	    me.id, SASL_SERVER, client->id, addr, addr);
    315 
    316 	if (certfp)
    317 		sendto_one(sasl_server, NULL, ":%s SASL %s %s S %s %s",
    318 		    me.id, SASL_SERVER, client->id, "PLAIN", certfp);
    319 	else
    320 		sendto_one(sasl_server, NULL, ":%s SASL %s %s S %s",
    321 		    me.id, SASL_SERVER, client->id, "PLAIN");
    322 
    323 	/* The rest is sent from authprompt_sasl_continuation() */
    324 
    325 	client->local->sasl_out++;
    326 }
    327 
    328 CMD_FUNC(cmd_auth)
    329 {
    330 	char *username = NULL;
    331 	char *password = NULL;
    332 	char *authbuf;
    333 
    334 	if (!SEUSER(client))
    335 	{
    336 		if (HasCapability(client, "sasl"))
    337 			sendnotice(client, "ERROR: Cannot use /AUTH when your client is doing SASL.");
    338 		else
    339 			sendnotice(client, "ERROR: /AUTH authentication request received before authentication prompt (too early!)");
    340 		return;
    341 	}
    342 
    343 	if ((parc < 2) || BadPtr(parv[1]) || !parse_nickpass(parv[1], &username, &password))
    344 	{
    345 		sendnotice(client, "ERROR: Syntax is: /AUTH <nickname>:<password>");
    346 		sendnotice(client, "Example: /AUTH mynick:secretpass");
    347 		return;
    348 	}
    349 
    350 	if (!SASL_SERVER)
    351 	{
    352 		sendnotice(client, "ERROR: SASL is not configured on this server, or services are down.");
    353 		// numeric instead? SERVICESDOWN?
    354 		return;
    355 	}
    356 
    357 	/* Presumably if the user is really fast, this could happen.. */
    358 	if (*client->local->sasl_agent || SEUSER(client)->authmsg)
    359 	{
    360 		sendnotice(client, "ERROR: Previous authentication request is still in progress. Please wait.");
    361 		return;
    362 	}
    363 
    364 	authbuf = make_authbuf(username, password);
    365 	if (!authbuf)
    366 	{
    367 		sendnotice(client, "ERROR: Internal error. Oversized username/password?");
    368 		return;
    369 	}
    370 
    371 	safe_strdup(SEUSER(client)->authmsg, authbuf);
    372 
    373 	send_first_auth(client);
    374 }
    375 
    376 void authprompt_tag_as_auth_required(Client *client, const char *reason)
    377 {
    378 	/* Allocate, and therefore indicate, that we are going to handle SASL for this user */
    379 	if (!SEUSER(client))
    380 		SetAPUser(client, safe_alloc(sizeof(APUser)));
    381 	safe_strdup(SEUSER(client)->reason, reason);
    382 }
    383 
    384 void authprompt_send_auth_required_message(Client *client)
    385 {
    386 	/* Send the standard-reply ACCOUNT_REQUIRED_TO_CONNECT if the client supports receiving it */
    387 	if (HasCapability(client, "standard-replies"))
    388 	{
    389 		const char *reason = SEUSER(client) && SEUSER(client)->reason ? SEUSER(client)->reason : NULL;
    390 		if (reason)
    391 			sendto_one(client, NULL, "FAIL * ACCOUNT_REQUIRED_TO_CONNECT :An account is required to connect: %s", reason);
    392 		else
    393 			sendto_one(client, NULL, "FAIL * ACCOUNT_REQUIRED_TO_CONNECT :An account is required to connect");
    394 	}
    395 
    396 	/* Display set::authentication-prompt::message */
    397 	sendnotice_multiline(client, cfg.message);
    398 }
    399 
    400 /* Called upon "place a host ban on this user" (eg: spamfilter, blacklist, ..) */
    401 int authprompt_place_host_ban(Client *client, int action, const char *reason, long duration)
    402 {
    403 	/* If it's a soft-xx action and the user is not logged in
    404 	 * and the user is not yet online, then we will handle this user.
    405 	 */
    406 	if (IsSoftBanAction(action) && !IsLoggedIn(client) && !IsUser(client) && cfg.enabled)
    407 	{
    408 		/* And tag the user */
    409 		authprompt_tag_as_auth_required(client, reason);
    410 		authprompt_send_auth_required_message(client);
    411 		return 1; /* pretend user is killed */
    412 	}
    413 	return 99; /* no action taken, proceed normally */
    414 }
    415 
    416 /** Called upon "check for KLINE/GLINE" */
    417 int authprompt_find_tkline_match(Client *client, TKL *tkl)
    418 {
    419 	/* If it's a soft-xx action and the user is not logged in
    420 	 * and the user is not yet online, then we will handle this user.
    421 	 */
    422 	if (cfg.enabled &&
    423 		TKLIsServerBan(tkl) &&
    424 	   (tkl->ptr.serverban->subtype & TKL_SUBTYPE_SOFT) &&
    425 	   !IsLoggedIn(client) &&
    426 	   !IsUser(client))
    427 	{
    428 		/* And tag the user */
    429 		authprompt_tag_as_auth_required(client, tkl->ptr.serverban->reason);
    430 		authprompt_send_auth_required_message(client);
    431 		return 1; /* pretend user is killed */
    432 	}
    433 	return 99; /* no action taken, proceed normally */
    434 }
    435 
    436 int authprompt_pre_connect(Client *client)
    437 {
    438 	/* If the user is tagged as auth required and not logged in, then.. */
    439 	if (SEUSER(client) && !IsLoggedIn(client) && cfg.enabled)
    440 	{
    441 		authprompt_send_auth_required_message(client);
    442 		return HOOK_DENY; /* do not process register_user() */
    443 	}
    444 
    445 	return HOOK_CONTINUE; /* no action taken, proceed normally */
    446 }
    447 
    448 int authprompt_sasl_continuation(Client *client, const char *buf)
    449 {
    450 	/* If it's not for us (eg: user is doing real SASL) then return 0. */
    451 	if (!SEUSER(client) || !SEUSER(client)->authmsg)
    452 		return 0;
    453 
    454 	if (!strcmp(buf, "+"))
    455 	{
    456 		Client *agent = find_client(client->local->sasl_agent, NULL);
    457 		if (agent)
    458 		{
    459 			sendto_one(agent, NULL, ":%s SASL %s %s C %s",
    460 				me.id, AGENT_SID(agent), client->id, SEUSER(client)->authmsg);
    461 		}
    462 		safe_free(SEUSER(client)->authmsg);
    463 	}
    464 	return 1; /* inhibit displaying of message */
    465 }
    466 
    467 int authprompt_sasl_result(Client *client, int success)
    468 {
    469 	/* If it's not for us (eg: user is doing real SASL) then return 0. */
    470 	if (!SEUSER(client))
    471 		return 0;
    472 
    473 	if (!success)
    474 	{
    475 		sendnotice_multiline(client, cfg.fail_message);
    476 		return 1;
    477 	}
    478 
    479 	if (client->user && !IsLoggedIn(client))
    480 	{
    481 		sendnotice_multiline(client, cfg.unconfirmed_message);
    482 		return 1;
    483 	}
    484 
    485 	/* Authentication was a success */
    486 	if (*client->name && client->user && *client->user->username && IsNotSpoof(client))
    487 	{
    488 		register_user(client);
    489 		/* User MAY be killed now. But since we 'return 1' below, it's safe */
    490 	}
    491 
    492 	return 1; /* inhibit success/failure message */
    493 }
    494 
    495 /** Override the default "Registration timeout" quit reason */
    496 int authprompt_pre_local_handshake_timeout(Client *client, const char **comment)
    497 {
    498 	if (SEUSER(client))
    499 	{
    500 		if (SEUSER(client)->reason)
    501 			*comment = SEUSER(client)->reason;
    502 		else
    503 			*comment = "Account required to connect";
    504 	}
    505 
    506 	return HOOK_CONTINUE;
    507 }