mirror of
https://github.com/unrealircd/unrealircd.git
synced 2026-07-08 20:03:14 +02:00
60c0ab8da2
$nick, $username, $realname, $ip, $account, $operlogin, $operclass,
$country_code (xx for unknown), $asn (0 for unknown).
Note that if a $variable fails to expand, eg $operlogin but the
user is not oper, then the vhost will not be applied. A warning
is sent to the vhost snomask (+s +v) in such a case.
Examples:
/* Set authenticated users to $account.example.org */
vhost { auto-login yes; vhost $account.example.org; mask { identified yes; } }
/* Obviously not really a good idea, but.. to illustrate: */
vhost { auto-login yes; vhost $country_code.example.org; mask *; }
Also, when vhost { } blocks are read and need to be matched, they
are read top-down now, which is the most logical way. First match wins.
All this needs testing :)
603 lines
16 KiB
C
603 lines
16 KiB
C
/*
|
|
* Unreal Internet Relay Chat Daemon, src/modules/vhost.c
|
|
* (C) 2000-2024 Carsten V. Munk, Bram Matthys 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"
|
|
|
|
/* Defines */
|
|
#define MSG_VHOST "VHOST"
|
|
|
|
/* Structs */
|
|
ModuleHeader MOD_HEADER
|
|
= {
|
|
"vhost",
|
|
"6.0",
|
|
"command /VHOST and vhost { } blocks",
|
|
"UnrealIRCd Team",
|
|
"unrealircd-6",
|
|
};
|
|
|
|
typedef struct ConfigItem_vhost ConfigItem_vhost;
|
|
struct ConfigItem_vhost {
|
|
ConfigItem_vhost *prev, *next;
|
|
int auto_login; /**< Auto-login users on connect? If they match 'auth' */
|
|
SecurityGroup *match; /**< Match criteria for user */
|
|
char *login; /**< Login name for 'VHOST login pass' */
|
|
AuthConfig *auth; /**< Password for 'VHOST login pass */
|
|
char *virtuser; /**< Virtual ident to set */
|
|
char *virthost; /**< Virtual host to set */
|
|
SWhois *swhois; /**< SWhois items to set */
|
|
};
|
|
|
|
/* Variables */
|
|
ConfigItem_vhost *conf_vhost = NULL;
|
|
|
|
/* Forward declarations */
|
|
void free_vhost_config(void);
|
|
int vhost_config_test(ConfigFile *conf, ConfigEntry *ce, int type, int *errs);
|
|
int vhost_config_run(ConfigFile *conf, ConfigEntry *ce, int type);
|
|
static int stats_vhost(Client *client, const char *flag);
|
|
ConfigItem_vhost *find_vhost(const char *name);
|
|
void do_vhost(Client *client, ConfigItem_vhost *vhost);
|
|
CMD_FUNC(cmd_vhost);
|
|
int vhost_auto_set(Client *client);
|
|
|
|
MOD_TEST()
|
|
{
|
|
MARK_AS_OFFICIAL_MODULE(modinfo);
|
|
HookAdd(modinfo->handle, HOOKTYPE_CONFIGTEST, 0, vhost_config_test);
|
|
return MOD_SUCCESS;
|
|
}
|
|
|
|
MOD_INIT()
|
|
{
|
|
MARK_AS_OFFICIAL_MODULE(modinfo);
|
|
HookAdd(modinfo->handle, HOOKTYPE_CONFIGRUN, 0, vhost_config_run);
|
|
HookAdd(modinfo->handle, HOOKTYPE_STATS, 0, stats_vhost);
|
|
HookAdd(modinfo->handle, HOOKTYPE_PRE_LOCAL_CONNECT, INT_MAX, vhost_auto_set);
|
|
CommandAdd(modinfo->handle, MSG_VHOST, cmd_vhost, MAXPARA, CMD_USER);
|
|
return MOD_SUCCESS;
|
|
}
|
|
|
|
MOD_LOAD()
|
|
{
|
|
return MOD_SUCCESS;
|
|
}
|
|
|
|
|
|
MOD_UNLOAD()
|
|
{
|
|
free_vhost_config();
|
|
return MOD_SUCCESS;
|
|
}
|
|
|
|
void free_vhost_config(void)
|
|
{
|
|
ConfigItem_vhost *e, *e_next;
|
|
|
|
for (e = conf_vhost; e; e = e_next)
|
|
{
|
|
SWhois *s, *s_next;
|
|
|
|
e_next = e->next;
|
|
|
|
safe_free(e->login);
|
|
Auth_FreeAuthConfig(e->auth);
|
|
safe_free(e->virthost);
|
|
safe_free(e->virtuser);
|
|
free_security_group(e->match);
|
|
for (s = e->swhois; s; s = s_next)
|
|
{
|
|
s_next = s->next;
|
|
safe_free(s->line);
|
|
safe_free(s->setby);
|
|
safe_free(s);
|
|
}
|
|
DelListItem(e, conf_vhost);
|
|
safe_free(e);
|
|
}
|
|
conf_vhost = NULL;
|
|
}
|
|
/** Test a vhost { } block in the configuration file */
|
|
int vhost_config_test(ConfigFile *conf, ConfigEntry *ce, int type, int *errs)
|
|
{
|
|
ConfigEntry *cep;
|
|
char fakehost[HOSTLEN*2];
|
|
char has_vhost = 0, has_login = 0, has_password = 0, has_mask = 0, has_match = 0;
|
|
char has_auto_login = 0;
|
|
int errors = 0;
|
|
|
|
/* We are only interested in vhost { } blocks */
|
|
if ((type != CONFIG_MAIN) || strcmp(ce->name, "vhost"))
|
|
return 0;
|
|
|
|
for (cep = ce->items; cep; cep = cep->next)
|
|
{
|
|
if (!strcmp(cep->name, "auto-login"))
|
|
{
|
|
has_auto_login = config_checkval(cep->value, CFG_YESNO);
|
|
}
|
|
else if (!strcmp(cep->name, "vhost"))
|
|
{
|
|
char *at, *tmp, *host, *p;
|
|
if (has_vhost)
|
|
{
|
|
config_warn_duplicate(cep->file->filename,
|
|
cep->line_number, "vhost::vhost");
|
|
continue;
|
|
}
|
|
has_vhost = 1;
|
|
if (!cep->value)
|
|
{
|
|
config_error_empty(cep->file->filename,
|
|
cep->line_number, "vhost", "vhost");
|
|
errors++;
|
|
continue;
|
|
}
|
|
/* Silly trick to replace $vars to xvars so to postpone
|
|
* variable matching to at runtime...
|
|
*/
|
|
strlcpy(fakehost, cep->value, sizeof(fakehost));
|
|
for (p = fakehost; *p; p++)
|
|
if (*p == '$')
|
|
*p = 'x';
|
|
if (!valid_vhost(fakehost))
|
|
{
|
|
/* Note that the error message needs to be on the
|
|
* original cep->value and not on fakehost.
|
|
*/
|
|
config_error("%s:%i: vhost::vhost contains illegal characters or is too long: '%s'",
|
|
cep->file->filename, cep->line_number, cep->value);
|
|
errors++;
|
|
}
|
|
}
|
|
else if (!strcmp(cep->name, "login"))
|
|
{
|
|
if (has_login)
|
|
{
|
|
config_warn_duplicate(cep->file->filename,
|
|
cep->line_number, "vhost::login");
|
|
}
|
|
has_login = 1;
|
|
if (!cep->value)
|
|
{
|
|
config_error_empty(cep->file->filename,
|
|
cep->line_number, "vhost", "login");
|
|
errors++;
|
|
continue;
|
|
}
|
|
}
|
|
else if (!strcmp(cep->name, "password"))
|
|
{
|
|
if (has_password)
|
|
{
|
|
config_warn_duplicate(cep->file->filename,
|
|
cep->line_number, "vhost::password");
|
|
}
|
|
has_password = 1;
|
|
if (!cep->value)
|
|
{
|
|
config_error_empty(cep->file->filename,
|
|
cep->line_number, "vhost", "password");
|
|
errors++;
|
|
continue;
|
|
}
|
|
if (Auth_CheckError(cep, 0) < 0)
|
|
errors++;
|
|
}
|
|
else if (!strcmp(cep->name, "mask"))
|
|
{
|
|
has_mask = 1;
|
|
test_match_block(conf, cep, &errors);
|
|
}
|
|
else if (!strcmp(cep->name, "match"))
|
|
{
|
|
has_match = 1;
|
|
test_match_block(conf, cep, &errors);
|
|
}
|
|
else if (!strcmp(cep->name, "swhois"))
|
|
{
|
|
/* multiple is ok */
|
|
}
|
|
else
|
|
{
|
|
config_error_unknown(cep->file->filename, cep->line_number,
|
|
"vhost", cep->name);
|
|
errors++;
|
|
}
|
|
}
|
|
|
|
if (!has_vhost)
|
|
{
|
|
config_error_missing(ce->file->filename, ce->line_number,
|
|
"vhost::vhost");
|
|
errors++;
|
|
}
|
|
|
|
if (!has_auto_login)
|
|
{
|
|
if (!has_login)
|
|
{
|
|
config_error_missing(ce->file->filename, ce->line_number,
|
|
"vhost::login");
|
|
errors++;
|
|
|
|
}
|
|
if (!has_password)
|
|
{
|
|
config_error_missing(ce->file->filename, ce->line_number,
|
|
"vhost::password");
|
|
errors++;
|
|
}
|
|
}
|
|
|
|
if (!has_mask && !has_match)
|
|
{
|
|
config_error_missing(ce->file->filename, ce->line_number,
|
|
"vhost::match");
|
|
errors++;
|
|
}
|
|
if (has_mask && has_match)
|
|
{
|
|
config_error("%s:%d: You cannot have both ::mask and ::match. "
|
|
"You should only use %s::match.",
|
|
ce->file->filename, ce->line_number, ce->name);
|
|
errors++;
|
|
}
|
|
|
|
if (has_auto_login && has_password)
|
|
{
|
|
config_error("%s:%d: If ::auto-login is set to 'yes' then you "
|
|
"cannot have a ::password configured. "
|
|
"Remove the password if you want to use auto-login.",
|
|
ce->file->filename, ce->line_number);
|
|
}
|
|
*errs = errors;
|
|
return errors ? -1 : 1;
|
|
}
|
|
|
|
/** Process a vhost { } block in the configuration file */
|
|
int vhost_config_run(ConfigFile *conf, ConfigEntry *ce, int type)
|
|
{
|
|
ConfigEntry *cep, *cepp;
|
|
ConfigItem_vhost *vhost;
|
|
|
|
/* We are only interested in vhost { } blocks */
|
|
if ((type != CONFIG_MAIN) || strcmp(ce->name, "vhost"))
|
|
return 0;
|
|
|
|
vhost = safe_alloc(sizeof(ConfigItem_vhost));
|
|
vhost->match = safe_alloc(sizeof(SecurityGroup));
|
|
|
|
for (cep = ce->items; cep; cep = cep->next)
|
|
{
|
|
if (!strcmp(cep->name, "auto-login"))
|
|
{
|
|
vhost->auto_login = config_checkval(cep->value, CFG_YESNO);
|
|
}
|
|
else if (!strcmp(cep->name, "vhost"))
|
|
{
|
|
char *user, *host;
|
|
user = strtok(cep->value, "@");
|
|
host = strtok(NULL, "");
|
|
if (!host)
|
|
safe_strdup(vhost->virthost, user);
|
|
else
|
|
{
|
|
safe_strdup(vhost->virtuser, user);
|
|
safe_strdup(vhost->virthost, host);
|
|
}
|
|
}
|
|
else if (!strcmp(cep->name, "login"))
|
|
safe_strdup(vhost->login, cep->value);
|
|
else if (!strcmp(cep->name, "password"))
|
|
vhost->auth = AuthBlockToAuthConfig(cep);
|
|
else if (!strcmp(cep->name, "match") || !strcmp(cep->name, "mask"))
|
|
{
|
|
conf_match_block(conf, cep, &vhost->match);
|
|
}
|
|
else if (!strcmp(cep->name, "swhois"))
|
|
{
|
|
SWhois *s;
|
|
if (cep->items)
|
|
{
|
|
for (cepp = cep->items; cepp; cepp = cepp->next)
|
|
{
|
|
s = safe_alloc(sizeof(SWhois));
|
|
safe_strdup(s->line, cepp->name);
|
|
safe_strdup(s->setby, "vhost");
|
|
AddListItem(s, vhost->swhois);
|
|
}
|
|
} else
|
|
if (cep->value)
|
|
{
|
|
s = safe_alloc(sizeof(SWhois));
|
|
safe_strdup(s->line, cep->value);
|
|
safe_strdup(s->setby, "vhost");
|
|
AddListItem(s, vhost->swhois);
|
|
}
|
|
}
|
|
}
|
|
AppendListItem(vhost, conf_vhost);
|
|
return 1;
|
|
}
|
|
|
|
static int stats_vhost(Client *client, const char *flag)
|
|
{
|
|
ConfigItem_vhost *vhosts;
|
|
NameValuePrioList *m;
|
|
|
|
if (strcmp(flag, "S") && strcasecmp(flag, "vhost"))
|
|
return 0; /* Not for us */
|
|
|
|
for (vhosts = conf_vhost; vhosts; vhosts = vhosts->next)
|
|
{
|
|
for (m = vhosts->match->printable_list; m; m = m->next)
|
|
{
|
|
sendtxtnumeric(client, "vhost %s%s%s %s %s",
|
|
vhosts->virtuser ? vhosts->virtuser : "",
|
|
vhosts->virtuser ? "@" : "",
|
|
vhosts->virthost,
|
|
vhosts->login,
|
|
namevalue_nospaces(m));
|
|
}
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
|
|
|
|
ConfigItem_vhost *find_vhost(const char *name)
|
|
{
|
|
ConfigItem_vhost *vhost;
|
|
|
|
for (vhost = conf_vhost; vhost; vhost = vhost->next)
|
|
{
|
|
if (!strcmp(name, vhost->login))
|
|
return vhost;
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
CMD_FUNC(cmd_vhost)
|
|
{
|
|
ConfigItem_vhost *vhost;
|
|
char login[HOSTLEN+1];
|
|
const char *password;
|
|
|
|
if (!MyUser(client))
|
|
return;
|
|
|
|
if ((parc < 2) || BadPtr(parv[1]))
|
|
{
|
|
sendnumeric(client, ERR_NEEDMOREPARAMS, "VHOST");
|
|
return;
|
|
|
|
}
|
|
|
|
/* cut-off too long login names. HOSTLEN is arbitrary, we just don't want our
|
|
* error messages to be cut off because the user is sending huge login names.
|
|
*/
|
|
strlcpy(login, parv[1], sizeof(login));
|
|
|
|
password = (parc > 2) ? parv[2] : "";
|
|
|
|
if (!(vhost = find_vhost(login)))
|
|
{
|
|
unreal_log(ULOG_WARNING, "vhost", "VHOST_FAILED", client,
|
|
"Failed VHOST attempt by $client.details [reason: $reason] [vhost-block: $vhost_block]",
|
|
log_data_string("reason", "Vhost block not found"),
|
|
log_data_string("fail_type", "UNKNOWN_VHOST_NAME"),
|
|
log_data_string("vhost_block", login));
|
|
sendnotice(client, "*** [\2vhost\2] Login for %s failed - password incorrect", login);
|
|
return;
|
|
}
|
|
|
|
if (!user_allowed_by_security_group(client, vhost->match))
|
|
{
|
|
unreal_log(ULOG_WARNING, "vhost", "VHOST_FAILED", client,
|
|
"Failed VHOST attempt by $client.details [reason: $reason] [vhost-block: $vhost_block]",
|
|
log_data_string("reason", "Host does not match"),
|
|
log_data_string("fail_type", "NO_HOST_MATCH"),
|
|
log_data_string("vhost_block", login));
|
|
sendnotice(client, "*** No vHost lines available for your host");
|
|
return;
|
|
}
|
|
|
|
if (!Auth_Check(client, vhost->auth, password))
|
|
{
|
|
unreal_log(ULOG_WARNING, "vhost", "VHOST_FAILED", client,
|
|
"Failed VHOST attempt by $client.details [reason: $reason] [vhost-block: $vhost_block]",
|
|
log_data_string("reason", "Authentication failed"),
|
|
log_data_string("fail_type", "AUTHENTICATION_FAILED"),
|
|
log_data_string("vhost_block", login));
|
|
sendnotice(client, "*** [\2vhost\2] Login for %s failed - password incorrect", login);
|
|
return;
|
|
}
|
|
|
|
/* Authentication passed, but.. there could still be other restrictions: */
|
|
switch (UHOST_ALLOWED)
|
|
{
|
|
case UHALLOW_NEVER:
|
|
if (MyUser(client))
|
|
{
|
|
sendnotice(client, "*** /vhost is disabled");
|
|
return;
|
|
}
|
|
break;
|
|
case UHALLOW_ALWAYS:
|
|
break;
|
|
case UHALLOW_NOCHANS:
|
|
if (MyUser(client) && client->user->joined)
|
|
{
|
|
sendnotice(client, "*** /vhost can not be used while you are on a channel");
|
|
return;
|
|
}
|
|
break;
|
|
case UHALLOW_REJOIN:
|
|
/* join sent later when the host has been changed */
|
|
break;
|
|
}
|
|
|
|
/* All checks passed, now let's go ahead and change the host */
|
|
do_vhost(client, vhost);
|
|
}
|
|
|
|
char *unreal_expand_standard_variables(const char *str, char *buf, size_t buflen, NameValuePrioList *nvp, Client *client)
|
|
{
|
|
const char *s;
|
|
|
|
if (client)
|
|
{
|
|
add_nvplist(&nvp, 0, "nick", client->name);
|
|
add_nvplist(&nvp, 0, "ip", GetIP(client));
|
|
if (client->user)
|
|
{
|
|
add_nvplist(&nvp, 0, "username", client->user->username);
|
|
add_nvplist(&nvp, 0, "realname", client->info);
|
|
add_nvplist(&nvp, 0, "account", client->user->account);
|
|
s = get_operlogin(client);
|
|
if (s)
|
|
add_nvplist(&nvp, 0, "operlogin", s);
|
|
s = get_operclass(client);
|
|
if (s)
|
|
add_nvplist(&nvp, 0, "operclass", s);
|
|
}
|
|
if (client->ip)
|
|
{
|
|
GeoIPResult *geo = geoip_client(client);
|
|
if (geo)
|
|
{
|
|
char asn[32];
|
|
if (geo->country_code)
|
|
add_nvplist(&nvp, 0, "country_code", geo->country_code);
|
|
else
|
|
add_nvplist(&nvp, 0, "country_code", "XX");
|
|
|
|
/* Safe to set this unconditionally, will simply be 0
|
|
* for things like localhost and such.
|
|
*/
|
|
snprintf(asn, sizeof(asn), "%d", geo->asn);
|
|
add_nvplist(&nvp, 0, "asn", asn);
|
|
} else {
|
|
add_nvplist(&nvp, 0, "country_code", "XX");
|
|
add_nvplist(&nvp, 0, "asn", "0");
|
|
}
|
|
}
|
|
}
|
|
buildvarstring_nvp(str, buf, buflen, nvp, 0);
|
|
strtolower(buf);
|
|
safe_free_nvplist(nvp);
|
|
return buf;
|
|
}
|
|
|
|
void do_vhost(Client *client, ConfigItem_vhost *vhost)
|
|
{
|
|
char olduser[USERLEN+1];
|
|
char newhost[HOSTLEN+1];
|
|
|
|
*newhost = '\0';
|
|
unreal_expand_standard_variables(vhost->virthost, newhost, sizeof(newhost), NULL, client);
|
|
if (!valid_vhost(newhost))
|
|
{
|
|
sendnotice(client, "*** Unable to apply vhost automatically");
|
|
if (vhost->auto_login)
|
|
{
|
|
unreal_log(ULOG_WARNING, "vhost", "AUTO_VHOST_FAILED", client,
|
|
"Unable to set auto-vhost on user. "
|
|
"Vhost '$vhost_format' expanded to '$newhost' but is invalid.",
|
|
log_data_string("vhost_format", vhost->virthost),
|
|
log_data_string("newhost", newhost));
|
|
}
|
|
return;
|
|
}
|
|
|
|
userhost_save_current(client);
|
|
|
|
safe_strdup(client->user->virthost, newhost);
|
|
if (vhost->virtuser)
|
|
{
|
|
strlcpy(olduser, client->user->username, sizeof(olduser));
|
|
strlcpy(client->user->username, vhost->virtuser, sizeof(client->user->username));
|
|
sendto_server(client, 0, 0, NULL, ":%s SETIDENT %s", client->id,
|
|
client->user->username);
|
|
}
|
|
client->umodes |= UMODE_HIDE;
|
|
client->umodes |= UMODE_SETHOST;
|
|
sendto_server(client, 0, 0, NULL, ":%s SETHOST %s", client->id, client->user->virthost);
|
|
sendto_one(client, NULL, ":%s MODE %s :+tx", client->name, client->name);
|
|
if (vhost->swhois)
|
|
{
|
|
SWhois *s;
|
|
for (s = vhost->swhois; s; s = s->next)
|
|
swhois_add(client, "vhost", -100, s->line, &me, NULL);
|
|
}
|
|
sendnotice(client, "*** Your vhost is now %s%s%s",
|
|
vhost->virtuser ? vhost->virtuser : "",
|
|
vhost->virtuser ? "@" : "",
|
|
newhost);
|
|
|
|
/* Only notify on logins, not on auto logins (should we make that configurable?)
|
|
* (if you do want it for auto logins, note that vhost->login will be NULL
|
|
* in the unreal_log() call currently below).
|
|
*/
|
|
if (vhost->login)
|
|
{
|
|
if (vhost->virtuser)
|
|
{
|
|
/* virtuser@virthost */
|
|
unreal_log(ULOG_INFO, "vhost", "VHOST_SUCCESS", client,
|
|
"$client.details is now using vhost $virtuser@$virthost [vhost-block: $vhost_block]",
|
|
log_data_string("virtuser", vhost->virtuser),
|
|
log_data_string("virthost", newhost),
|
|
log_data_string("vhost_block", vhost->login));
|
|
} else {
|
|
/* just virthost */
|
|
unreal_log(ULOG_INFO, "vhost", "VHOST_SUCCESS", client,
|
|
"$client.details is now using vhost $virthost [vhost-block: $vhost_block]",
|
|
log_data_string("virthost", newhost),
|
|
log_data_string("vhost_block", vhost->login));
|
|
}
|
|
}
|
|
|
|
userhost_changed(client);
|
|
}
|
|
|
|
int vhost_auto_set(Client *client)
|
|
{
|
|
ConfigItem_vhost *vhost;
|
|
|
|
if (IsSetHost(client))
|
|
return 0; /* Don't override if e.g. anope already set a vhost */
|
|
|
|
for (vhost = conf_vhost; vhost; vhost = vhost->next)
|
|
{
|
|
if (vhost->auto_login &&
|
|
!vhost->auth &&
|
|
vhost->match &&
|
|
user_allowed_by_security_group(client, vhost->match))
|
|
{
|
|
do_vhost(client, vhost);
|
|
return 0;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|