/* +------------------------------------+
* | Inspire Internet Relay Chat Daemon |
* +------------------------------------+
*
* InspIRCd: (C) 2002-2011 InspIRCd Development Team
* See: http://wiki.inspircd.org/Credits
*
* This program is free but copyrighted software; see
* the file COPYING for details.
*
* ---------------------------------------------------
*/
#include "inspircd.h"
#include "account.h"
#include "hash.h"
/* $ModDesc: Read and write accounts from a flat-file database */
static dynamic_reference<AccountProvider> account("account");
static dynamic_reference<AccountDBProvider> db("accountdb");
/******************************************************************************
* Flat-file database read/write
******************************************************************************/
/** reads the account database and returns structure with it */
class DatabaseReader
{
/* file */
FILE *fd;
public:
/* constructor, opens the file */
DatabaseReader (std::string filename)
{
/* initialize */
fd = NULL;
/* open the file */
fd = fopen (filename.c_str ( ), "r");
/* if we can't open the file, return. */
if (!fd) return;
}
/* destructor will close the file */
~DatabaseReader ( )
{
/* if fd is not null, close it */
if (fd) fclose (fd);
}
/* read and store next entry */
bool next ( )
{
/* if fd is NULL, fake eof */
if (!fd) return false;
std::string str;
/* read single characters from the file and add them to the string, end at EOF or \n, ignore \r */
while (1)
{
int c = fgetc (fd);
if ((c == EOF) || ((unsigned char)c == '\n')) break;
if ((unsigned char)c == '\r') continue;
str.push_back ((unsigned char)c);
}
/* ready to parse the line */
if (str == "") return false;
irc::string name;
time_t ts, hash_password_ts;
std::string hash, password;
std::map<std::string, std::string> extensions;
std::string token;
irc::spacesepstream sep(str, false);
/* get first one */
/* malformed if it is not acctinfo */
if (!sep.GetToken (token) || token != "acctinfo")
{
ServerInstance->Logs->Log ("MODULE", DEFAULT, "malformed account database - entry didn't start with acctinfo");
return false;
}
/* read account name */
/* if we don't have one, database is malformed */
if (!sep.GetToken (name))
{
ServerInstance->Logs->Log ("MODULE", DEFAULT, "malformed account database - ran out of tokens, expected name");
return false;
}
/* read account TS */
/* if we don't have one, database is malformed */
if (!sep.GetToken (token))
{
ServerInstance->Logs->Log ("MODULE", DEFAULT, "malformed account database - ran out of tokens, expected ts");
return false;
}
ts = atol(token.c_str());
/* read hash type */
/* if we don't have one, database is malformed */
if (!sep.GetToken (hash))
{
ServerInstance->Logs->Log ("MODULE", DEFAULT, "malformed account database - ran out of tokens, expected hash");
return false;
}
/* read password */
/* if we don't have one, database is malformed */
if (!sep.GetToken (password))
{
ServerInstance->Logs->Log ("MODULE", DEFAULT, "malformed account database - ran out of tokens, expected password");
return false;
}
/* read hash and password TS */
/* if we don't have one, database is malformed */
if (!sep.GetToken (token))
{
ServerInstance->Logs->Log ("MODULE", DEFAULT, "malformed account database - ran out of tokens, expected hash/password ts");
return false;
}
hash_password_ts = atol(token.c_str());
/* initial entry read, read next lines in a loop until end, eof means malformed database again */
while (1)
{
str.clear ( );
/* read the line */
while (1)
{
int c = fgetc (fd);
if (c == EOF)
{
ServerInstance->Logs->Log ("MODULE", DEFAULT, "malformed account database - eof, expected end");
return false;
}
unsigned char c2 = (unsigned char)c;
if (c2 == '\n') break;
if (c2 == '\r') continue;
str.push_back (c2);
}
irc::spacesepstream sep2(str, false);
/* get the token */
if (str == "")
{
ServerInstance->Logs->Log ("MODULE", DEFAULT, "malformed account database - empty line");
return false;
}
sep2.GetToken (token);
/* break the loop if token is "end" */
if (token == "end") break;
extensions.insert(std::make_pair(token, sep2.GetRemaining()));
}
AccountDBEntry* entry = db->GetAccount(name, false);
if(!entry || entry->ts > ts)
{
if(entry)
db->RemoveAccount(false, entry);
entry = db->AddAccount(false, name, ts, hash, password, hash_password_ts);
}
else if(entry->ts == ts)
{
if(hash_password_ts > entry->hash_password_ts)
{
entry->hash = hash;
entry->password = password;
entry->hash_password_ts = hash_password_ts;
}
}
else
return true;
for(std::map<std::string, std::string>::iterator i = extensions.begin(); i != extensions.end(); ++i)
{
ExtensionItem* ext = ServerInstance->Extensions.GetItem(i->first);
if (ext) ext->unserialize(FORMAT_PERSIST, entry, i->second);
}
db->SendAccount(entry);
return true;
}
};
/* class being a database writer, gets a database file name on construct */
class DatabaseWriter
{
/* file stream */
FILE *fd;
std::string dbname, tmpname;
/* public */
public:
/* constructor */
DatabaseWriter (std::string filename)
{
fd = NULL;
dbname = filename;
tmpname = filename + ".tmp";
/* ready, open temporary database */
fd = fopen (tmpname.c_str ( ), "w");
if (!fd)
{
ServerInstance->Logs->Log ("MODULE", DEFAULT, "cannot save to the account database");
}
}
/* destructor */
~DatabaseWriter ( )
{
if (fd)
{
/* saving has been ended, close the database and flush buffers */
fclose (fd);
/* rename the database file */
if (rename (tmpname.c_str ( ), dbname.c_str ( )) == -1)
{
ServerInstance->Logs->Log ("MODULE", DEFAULT, "can't rename the database file");
}
}
}
/* save the single entry */
void next (const AccountDBEntry* ent)
{
if (!fd) return;
/* first, construct the acctinfo line */
std::string line;
line.append("acctinfo ").append (ent->name).append (" ").append (ConvToStr(ent->ts)).append (" ")
.append (ent->hash).append (" ").append (ent->password).append (" ")
.append (ConvToStr(ent->hash_password_ts)).append ("\n");
if (fputs (line.c_str ( ), fd) == EOF)
{
ServerInstance->Logs->Log ("MODULE", DEFAULT, "unable to write the account entry");
fclose (fd);
fd = NULL;
return;
}
for(Extensible::ExtensibleStore::const_iterator i = ent->GetExtList().begin(); i != ent->GetExtList().end(); ++i)
{
ExtensionItem* item = i->first;
std::string value = item->serialize(FORMAT_PERSIST, ent, i->second);
if (!value.empty())
{
std::string toWrite = item->name + " " + value + "\n";
if (fputs (toWrite.c_str ( ), fd) == EOF)
{
ServerInstance->Logs->Log ("MODULE", DEFAULT, "can't write extension item entry");
fclose (fd);
fd = NULL;
return;
}
}
}
if (fputs ("end\n", fd) == EOF)
{
ServerInstance->Logs->Log ("MODULE", DEFAULT, "can't write end of account entry");
fclose (fd);
fd = NULL;
return;
}
}
};
class ModuleFlatfileAccount : public Module
{
private:
std::string dbfile;
bool dirty; // dbfile needs to be flushed to disk
void WriteFileDatabase ( )
{
// Dump entire database; open/close in constructor/destructor
DatabaseWriter dbwriter (dbfile);
for (AccountDB::const_iterator i = db->GetDB().begin(); i != db->GetDB().end(); ++i)
dbwriter.next(i->second);
}
public:
ModuleFlatfileAccount() : dirty(true)
{
}
void init()
{
if(!db) throw ModuleException("m_flatfile_account requires that m_account be loaded");
Implementation eventlist[] = { I_OnBackgroundTimer, I_OnEvent };
ServerInstance->Modules->Attach(eventlist, this, sizeof(eventlist)/sizeof(Implementation));
for(DatabaseReader dbreader(dbfile);dbreader.next();) {}
}
void ReadConfig(ConfigReadStatus&)
{
ConfigTag* conf = ServerInstance->Config->GetTag("flatfileacct");
dbfile = conf->getString("dbfile");
}
/* called on background timer to write all accounts to disk if they were changed */
void OnBackgroundTimer (time_t cur)
{
/* if not dirty then don't do anything */
if (!dirty)
return;
/* dirty, an account was changed, save it */
if(db) WriteFileDatabase();
/* clear dirty to prevent next savings */
dirty = false;
}
void OnEvent(Event& event)
{
if(event.id == "accountdb_modified") dirty = true;
}
void Prioritize()
{
// database reading may depend on extension item providers being loaded
ServerInstance->Modules->SetPriority(this, I_ModuleInit, PRIORITY_LAST);
}
Version GetVersion()
{
return Version("Read and write accounts from a flat-file database", VF_VENDOR);
}
};
MODULE_INIT(ModuleFlatfileAccount)