ircd/nickserv.cpp
#include "nickserv.hpp"
#include "irc_std.hpp"
#include "config.hpp"
#include "common.hpp"
#define NICKSERV_NICK "NickServ"
#ifdef USE_NICKSERV
#include "client_state.hpp"
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <assert.h>
#define CRYPT_SALT "$6$nickserv$" // SHA512
#define PWLEN 256
static const char* nick_basename(const char* nick) { ///< strips trailing '_'s
#if 1
return nick;
#else
static char buf[128];
size_t l = strlen(nick);
while (l && nick[l-1] == '_') {
l--;
}
if (l >= sizeof(buf)) {
assert(false);
return nick;
}
strncpy(buf, nick, l);
buf[l] = '\0';
return buf;
#endif
}
NickServ::NickServ(const char* spooldir) {
if (!spooldir || !*spooldir) {
dirfd = -1;
} else {
dirfd = open(spooldir, O_DIRECTORY|O_RDONLY);
if (dirfd == -1) {
LOG_ERRNO("open(%s, DIR)", spooldir);
} else {
LOG(NICKSERV_NICK" database at %s/", spooldir);
}
}
}
NickServ::~NickServ() {
inst = NULL;
if (dirfd != -1) {
close(dirfd);
}
}
bool NickServ::has_pw(const char* nick) const {
assert(dirfd != -1);
static struct stat ss;
if (!ClientState::is_nick(nick)) {
return false;
}
if (fstatat(dirfd, nick, &ss, 0) == -1) {
return false;
}
return S_ISREG(ss.st_mode);
}
const char* NickServ::get_pw(const char* nick) const {
assert(dirfd != -1);
if (!ClientState::is_nick(nick)) {
return NULL;
}
int fd = openat(dirfd, nick, O_RDONLY);
if (fd == -1) {
LOG_ERRNO("open(%s, r)", nick);
return NULL;
}
struct stat ss;
if (fstat(fd, &ss) == -1) {
LOG_ERRNO("fstat(%s)", nick);
close(fd);
return NULL;
}
if (ss.st_size > PWLEN) {
LOG("st_size(%s): %lld", nick, (long long)ss.st_size);
close(fd);
return NULL;
}
const size_t buflen = (size_t)ss.st_size;
static char buf[PWLEN+1];
ssize_t rv = read(fd, buf, buflen);
if (rv == -1) {
LOG_ERRNO("read(%s)", nick);
close(fd);
return NULL;
} else if ((size_t)rv != buflen) {
LOG("read(%s): %zd", nick, rv);
close(fd);
return NULL;
}
buf[buflen] = '\0';
close(fd);
return buf;
}
bool NickServ::set_pw(const char* nick, const char* pw) {
assert(dirfd != -1);
if (!ClientState::is_nick(nick)) {
return false;
}
if (!pw) {
if (unlinkat(dirfd, nick, 0) == -1) {
LOG_ERRNO("unlink(%s)", nick);
return false;
}
return true;
}
int fd = openat(dirfd, nick, O_WRONLY|O_CREAT|O_EXCL|O_TRUNC, S_IRUSR|S_IWUSR); // don't allow change, only re-registration
if (fd == -1) {
LOG_ERRNO("open(%s, w)", nick);
return false;
}
#ifndef NO_USE_CRYPT
pw = crypt(pw, CRYPT_SALT); // link with -lcrypt
if (!pw) {
LOG_ERRNO("crypt()");
return false;
}
#endif
size_t pwlen = strlen(pw);
if (pwlen > PWLEN) {
return false;
}
ssize_t rv = write(fd, pw, pwlen);
if (rv == -1) {
LOG_ERRNO("write(%s)", nick);
(void)unlinkat(dirfd, nick, 0);
close(fd);
return false;
} else if ((size_t)rv != pwlen) {
LOG("write(%s): %zd", nick, rv);
(void)unlinkat(dirfd, nick, 0);
close(fd);
return false;
} else {
close(fd);
return true;
}
}
bool NickServ::check_pw(const char* nick, const char* pw) const {
const char* cpw = get_pw(nick);
if (!cpw) {
return false;
}
#ifdef NO_USE_CRYPT
return (strcmp(cpw, pw) == 0);
#else
const char* md = crypt(pw, CRYPT_SALT);
return (md && strcmp(cpw, md) == 0);
#endif
}
bool NickServ::reply(Client* client, const char* msg) const {
(void)srv_reply_fmt(client, "NOTICE", NICKSERV_NICK ": %s", msg);
return raw_reply_fmt_long(client, msg, ":%s PRIVMSG %s", NICKSERV_NICK, client->irc_state->nick);
}
bool NickServ::reg(Client* client, const char* pw) {
if (client->irc_state->nickserv_reg) {
return reply(client, "Your nick has already been registered.");
}
assert(!client->irc_state->nickserv_ident);
if (!*pw || strcmp(pw, client->irc_state->nick) == 0 || (config.server_pw.str && strcmp(pw, config.server_pw.str) == 0)) {
return reply(client, "Don't use this password.");
}
if (!set_pw(nick_basename(client->irc_state->nick), pw)) {
return reply(client, "Error setting password.");
}
client->irc_state->nickserv_reg = true;
client->irc_state->nickserv_ident = true; // and also mark as identified
(void)reply(client, "Your nick has been registered. Reconnecting.");
return false;
}
bool NickServ::ident(Client* client, const char* pw) {
if (!client->irc_state->nickserv_reg) {
return reply(client, "Your nick has not been registered.");
}
if (client->irc_state->nickserv_ident) {
return reply(client, "You are already identified.");
}
if (!check_pw(nick_basename(client->irc_state->nick), pw)) {
return reply(client, "Password error or mismatch.");
}
client->irc_state->nickserv_ident = true;
return true; // silent if common case is ok
}
bool NickServ::drop(Client* client) {
if (!client->irc_state->nickserv_reg) {
return reply(client, "Your nick has not been registered.");
}
if (!client->irc_state->nickserv_ident) { // thus double auth?
return reply(client, "You are not yet identified.");
}
if (!set_pw(nick_basename(client->irc_state->nick), NULL)) {
return reply(client, "Error removing password.");
}
client->irc_state->nickserv_reg = false;
client->irc_state->nickserv_ident = false;
(void)reply(client, "Your nick has been unregistered. Reconnecting.");
return false;
}
bool NickServ::is_reg(const char* nick) {
if (dirfd == -1) { // or not enabled
return false;
}
return has_pw(nick_basename(nick));
}
bool NickServ::is_enabled() {
NickServ* i = getInst();
return i->dirfd != -1; // enabled needed?
}
bool NickServ::handle(Client* client, const char* params[], unsigned nparams) {
if (dirfd == -1) {
return srv_reply_fmt(client, REPLYFMT(ERR_NOSUCHNICK), NICKSERV_NICK);
}
if (nparams < 1) {
return reply(client, "Unknown " NICKSERV_NICK " command (register|identify|drop).");
}
const char* cmd = params[0];
if (strcasecmp(cmd, "register") == 0) {
if (nparams < 2) {
return reply(client, "usage: register [NICK] PASS [EMAIL]");
} else if (nparams == 2) {
return reg(client, params[1]);
} else if (strcmp(params[1], client->irc_state->nick) == 0) {
return reg(client, params[2]);
} else {
return reg(client, params[1]);
}
} else if (strcasecmp(cmd, "identify") == 0) {
if (nparams < 2) {
return reply(client, "usage: identify [NICK] PASS");
} else if (nparams == 2) {
return ident(client, params[1]);
} else {
return ident(client, params[2]);
}
} else if (strcasecmp(cmd, "drop") == 0) { // drop [NICK] [PASS]
return drop(client);
} else {
return reply(client, "Unknown " NICKSERV_NICK " command [register|identify|drop].");
}
}
#else
NickServ::NickServ(const char* spooldir) {
if (spooldir && *spooldir) {
LOG(NICKSERV_NICK" configured, but support disabled!");
}
}
NickServ::~NickServ() {
inst = NULL;
}
bool NickServ::handle(Client* client, const char* params[], unsigned nparams) {
return srv_reply_fmt(client, REPLYFMT(ERR_NOSUCHNICK), NICKSERV_NICK);
}
bool NickServ::is_reg(const char* nick) {
return false;
}
bool NickServ::is_enabled() {
return false;
}
#endif
NickServ* NickServ::inst = NULL;
NickServ* NickServ::getInst() {
return inst ?: (inst = new NickServ(config.nickserv_dir.str));
}