/* * 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.1.8.1", "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; int has_vhost = 0, has_login = 0, has_password = 0, has_mask = 0, has_match = 0; int 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; config_detect_duplicate(&has_vhost, cep, &errors); if (!cep->value) { config_error_empty(cep->file->filename, cep->line_number, "vhost", "vhost"); errors++; continue; } if (!potentially_valid_vhost(cep->value)) { /* 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")) { config_detect_duplicate(&has_login, cep, &errors); if (!cep->value) { config_error_empty(cep->file->filename, cep->line_number, "vhost", "login"); errors++; continue; } } else if (!strcmp(cep->name, "password")) { config_detect_duplicate(&has_password, cep, &errors); 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")) AuthBlockToAuthConfig(cep, &vhost->auth); 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, "V") && 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 ? 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 (vhost->login && !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); } void do_vhost(Client *client, ConfigItem_vhost *vhost) { char olduser[USERLEN+1]; char newhost[HOSTLEN+1]; /* There are various IsUser() checks in the code below, that is because * this code is also called for CLIENT_STATUS_UNKNOWN users in the handshake * that have not yet been introduced to other servers. For such users we * should not send SETIDENT and SETHOST messages out... such info will * be sent in the UID message when the user is introduced. */ *newhost = '\0'; unreal_expand_string(vhost->virthost, newhost, sizeof(newhost), NULL, 0, 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 $client.details. " "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)); if (IsUser(client)) sendto_server(client, 0, 0, NULL, ":%s SETIDENT %s", client->id, client->user->username); } client->umodes |= UMODE_HIDE; client->umodes |= UMODE_SETHOST; if (IsUser(client)) { 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); } if (IsUser(client)) { 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; }