00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014 #include "module.h"
00015 #include "os_session.h"
00016
00017 class MySessionService : public SessionService
00018 {
00019 SessionMap Sessions;
00020 Serialize::Checker<ExceptionVector> Exceptions;
00021 public:
00022 MySessionService(Module *m) : SessionService(m), Exceptions("Exception") { }
00023
00024 void AddException(Exception *e) anope_override
00025 {
00026 this->Exceptions->push_back(e);
00027 }
00028
00029 void DelException(Exception *e) anope_override
00030 {
00031 ExceptionVector::iterator it = std::find(this->Exceptions->begin(), this->Exceptions->end(), e);
00032 if (it != this->Exceptions->end())
00033 this->Exceptions->erase(it);
00034 }
00035
00036 Exception *FindException(User *u) anope_override
00037 {
00038 for (std::vector<Exception *>::const_iterator it = this->Exceptions->begin(), it_end = this->Exceptions->end(); it != it_end; ++it)
00039 {
00040 Exception *e = *it;
00041 if (Anope::Match(u->host, e->mask) || Anope::Match(u->ip, e->mask))
00042 return e;
00043 }
00044 return NULL;
00045 }
00046
00047 Exception *FindException(const Anope::string &host) anope_override
00048 {
00049 for (std::vector<Exception *>::const_iterator it = this->Exceptions->begin(), it_end = this->Exceptions->end(); it != it_end; ++it)
00050 {
00051 Exception *e = *it;
00052 if (Anope::Match(host, e->mask))
00053 return e;
00054 }
00055
00056 return NULL;
00057 }
00058
00059 ExceptionVector &GetExceptions() anope_override
00060 {
00061 return this->Exceptions;
00062 }
00063
00064 void AddSession(Session *s) anope_override
00065 {
00066 this->Sessions[s->addr] = s;
00067 }
00068
00069 void DelSession(Session *s) anope_override
00070 {
00071 this->Sessions.erase(s->addr);
00072 }
00073
00074 Session *FindSession(const Anope::string &ip) anope_override
00075 {
00076 cidr c(ip, ip.find(':') != Anope::string::npos ? Config->SessionIPv6CIDR : Config->SessionIPv4CIDR);
00077 SessionMap::iterator it = this->Sessions.find(c);
00078 if (it != this->Sessions.end())
00079 return it->second;
00080 return NULL;
00081 }
00082
00083 SessionMap &GetSessions() anope_override
00084 {
00085 return this->Sessions;
00086 }
00087 };
00088
00089 class ExpireTimer : public Timer
00090 {
00091 public:
00092 ExpireTimer() : Timer(Config->ExpireTimeout, Anope::CurTime, true) { }
00093
00094 void Tick(time_t) anope_override
00095 {
00096 if (!session_service || Anope::NoExpire)
00097 return;
00098 for (unsigned i = session_service->GetExceptions().size(); i > 0; --i)
00099 {
00100 Exception *e = session_service->GetExceptions()[i - 1];
00101
00102 if (!e->expires || e->expires > Anope::CurTime)
00103 continue;
00104 Log(OperServ, "expire/exception") << "Session exception for " << e->mask << "has expired.";
00105 session_service->DelException(e);
00106 delete e;
00107 }
00108 }
00109 };
00110
00111 class ExceptionDelCallback : public NumberList
00112 {
00113 protected:
00114 CommandSource &source;
00115 unsigned deleted;
00116 public:
00117 ExceptionDelCallback(CommandSource &_source, const Anope::string &numlist) : NumberList(numlist, true), source(_source), deleted(0)
00118 {
00119 }
00120
00121 ~ExceptionDelCallback()
00122 {
00123 if (!deleted)
00124 source.Reply(_("No matching entries on session-limit exception list."));
00125 else if (deleted == 1)
00126 source.Reply(_("Deleted 1 entry from session-limit exception list."));
00127 else
00128 source.Reply(_("Deleted %d entries from session-limit exception list."), deleted);
00129 }
00130
00131 virtual void HandleNumber(unsigned number) anope_override
00132 {
00133 if (!number || number > session_service->GetExceptions().size())
00134 return;
00135
00136 ++deleted;
00137
00138 DoDel(source, number - 1);
00139 }
00140
00141 static void DoDel(CommandSource &source, unsigned index)
00142 {
00143 Exception *e = session_service->GetExceptions()[index];
00144 FOREACH_MOD(I_OnExceptionDel, OnExceptionDel(source, e));
00145
00146 session_service->DelException(e);
00147 delete e;
00148 }
00149 };
00150
00151 class CommandOSSession : public Command
00152 {
00153 private:
00154 void DoList(CommandSource &source, const std::vector<Anope::string> ¶ms)
00155 {
00156 Anope::string param = params[1];
00157
00158 unsigned mincount = 0;
00159 try
00160 {
00161 mincount = convertTo<unsigned>(param);
00162 }
00163 catch (const ConvertException &) { }
00164
00165 if (mincount <= 1)
00166 source.Reply(_("Invalid threshold value. It must be a valid integer greater than 1."));
00167 else
00168 {
00169 ListFormatter list;
00170 list.AddColumn("Session").AddColumn("Host");
00171
00172 for (SessionService::SessionMap::iterator it = session_service->GetSessions().begin(), it_end = session_service->GetSessions().end(); it != it_end; ++it)
00173 {
00174 Session *session = it->second;
00175
00176 if (session->count >= mincount)
00177 {
00178 ListFormatter::ListEntry entry;
00179 entry["Session"] = stringify(session->count);
00180 entry["Host"] = session->addr.mask();
00181 list.AddEntry(entry);
00182 }
00183 }
00184
00185 source.Reply(_("Hosts with at least \002%d\002 sessions:"), mincount);
00186
00187 std::vector<Anope::string> replies;
00188 list.Process(replies);
00189
00190
00191 for (unsigned i = 0; i < replies.size(); ++i)
00192 source.Reply(replies[i]);
00193 }
00194
00195 return;
00196 }
00197
00198 void DoView(CommandSource &source, const std::vector<Anope::string> ¶ms)
00199 {
00200 Anope::string param = params[1];
00201 Session *session = NULL;
00202
00203 try
00204 {
00205 session = session_service->FindSession(param);
00206 }
00207 catch (const SocketException &) { }
00208
00209 if (!session)
00210 source.Reply(_("\002%s\002 not found on session list."), param.c_str());
00211 else
00212 {
00213 Exception *exception = session_service->FindException(param);
00214 source.Reply(_("The host \002%s\002 currently has \002%d\002 sessions with a limit of \002%d\002."), session->addr.mask().c_str(), session->count, exception && exception->limit > Config->DefSessionLimit ? exception->limit : Config->DefSessionLimit);
00215 }
00216
00217 return;
00218 }
00219 public:
00220 CommandOSSession(Module *creator) : Command(creator, "operserv/session", 2, 2)
00221 {
00222 this->SetDesc(_("View the list of host sessions"));
00223 this->SetSyntax(_("LIST \037threshold\037"));
00224 this->SetSyntax(_("VIEW \037host\037"));
00225 }
00226
00227 void Execute(CommandSource &source, const std::vector<Anope::string> ¶ms) anope_override
00228 {
00229 const Anope::string &cmd = params[0];
00230
00231 if (!Config->LimitSessions)
00232 {
00233 source.Reply(_("Session limiting is disabled."));
00234 return;
00235 }
00236
00237 if (cmd.equals_ci("LIST"))
00238 return this->DoList(source, params);
00239 else if (cmd.equals_ci("VIEW"))
00240 return this->DoView(source, params);
00241 else
00242 this->OnSyntaxError(source, "");
00243
00244 return;
00245 }
00246
00247 bool OnHelp(CommandSource &source, const Anope::string &subcommand) anope_override
00248 {
00249 this->SendSyntax(source);
00250 source.Reply(" ");
00251 source.Reply(_("Allows Services Operators to view the session list.\n"
00252 "\002SESSION LIST\002 lists hosts with at least \037threshold\037 sessions.\n"
00253 "The threshold must be a number greater than 1. This is to\n"
00254 "prevent accidental listing of the large number of single\n"
00255 "session hosts.\n"
00256 "\002SESSION VIEW\002 displays detailed information about a specific\n"
00257 "host - including the current session count and session limit.\n"
00258 "The \037host\037 value may not include wildcards.\n"
00259 "See the \002EXCEPTION\002 help for more information about session\n"
00260 "limiting and how to set session limits specific to certain\n"
00261 "hosts and groups thereof."));
00262 return true;
00263 }
00264 };
00265
00266 class CommandOSException : public Command
00267 {
00268 private:
00269 void DoAdd(CommandSource &source, const std::vector<Anope::string> ¶ms)
00270 {
00271 Anope::string mask, expiry, limitstr;
00272 unsigned last_param = 3;
00273
00274 mask = params.size() > 1 ? params[1] : "";
00275 if (!mask.empty() && mask[0] == '+')
00276 {
00277 expiry = mask;
00278 mask = params.size() > 2 ? params[2] : "";
00279 last_param = 4;
00280 }
00281
00282 limitstr = params.size() > last_param - 1 ? params[last_param - 1] : "";
00283
00284 if (params.size() <= last_param)
00285 {
00286 this->OnSyntaxError(source, "ADD");
00287 return;
00288 }
00289
00290 Anope::string reason = params[last_param];
00291 if (last_param == 3 && params.size() > 4)
00292 reason += " " + params[4];
00293 if (reason.empty())
00294 {
00295 this->OnSyntaxError(source, "ADD");
00296 return;
00297 }
00298
00299 time_t expires = !expiry.empty() ? Anope::DoTime(expiry) : Config->ExceptionExpiry;
00300 if (expires < 0)
00301 {
00302 source.Reply(BAD_EXPIRY_TIME);
00303 return;
00304 }
00305 else if (expires > 0)
00306 expires += Anope::CurTime;
00307
00308 unsigned limit = -1;
00309 try
00310 {
00311 limit = convertTo<unsigned>(limitstr);
00312 }
00313 catch (const ConvertException &) { }
00314
00315 if (limit > Config->MaxSessionLimit)
00316 {
00317 source.Reply(_("Invalid session limit. It must be a valid integer greater than or equal to zero and less than \002%d\002."), Config->MaxSessionLimit);
00318 return;
00319 }
00320 else
00321 {
00322 if (mask.find('!') != Anope::string::npos || mask.find('@') != Anope::string::npos)
00323 {
00324 source.Reply(_("Invalid hostmask. Only real hostmasks are valid, as exceptions are not matched against nicks or usernames."));
00325 return;
00326 }
00327
00328 for (std::vector<Exception *>::iterator it = session_service->GetExceptions().begin(), it_end = session_service->GetExceptions().end(); it != it_end; ++it)
00329 {
00330 Exception *e = *it;
00331 if (e->mask.equals_ci(mask))
00332 {
00333 if (e->limit != limit)
00334 {
00335 e->limit = limit;
00336 source.Reply(_("Exception for \002%s\002 has been updated to %d."), mask.c_str(), e->limit);
00337 }
00338 else
00339 source.Reply(_("\002%s\002 already exists on the EXCEPTION list."), mask.c_str());
00340 return;
00341 }
00342 }
00343
00344 Exception *exception = new Exception();
00345 exception->mask = mask;
00346 exception->limit = limit;
00347 exception->reason = reason;
00348 exception->time = Anope::CurTime;
00349 exception->who = source.GetNick();
00350 exception->expires = expires;
00351
00352 EventReturn MOD_RESULT;
00353 FOREACH_RESULT(I_OnExceptionAdd, OnExceptionAdd(exception));
00354 if (MOD_RESULT == EVENT_STOP)
00355 delete exception;
00356 else
00357 {
00358 session_service->AddException(exception);
00359 source.Reply(_("Session limit for \002%s\002 set to \002%d\002."), mask.c_str(), limit);
00360 if (Anope::ReadOnly)
00361 source.Reply(READ_ONLY_MODE);
00362 }
00363 }
00364
00365 return;
00366 }
00367
00368 void DoDel(CommandSource &source, const std::vector<Anope::string> ¶ms)
00369 {
00370 const Anope::string &mask = params.size() > 1 ? params[1] : "";
00371
00372 if (mask.empty())
00373 {
00374 this->OnSyntaxError(source, "DEL");
00375 return;
00376 }
00377
00378 if (isdigit(mask[0]) && mask.find_first_not_of("1234567890,-") == Anope::string::npos)
00379 {
00380 ExceptionDelCallback list(source, mask);
00381 list.Process();
00382 }
00383 else
00384 {
00385 unsigned i = 0, end = session_service->GetExceptions().size();
00386 for (; i < end; ++i)
00387 if (mask.equals_ci(session_service->GetExceptions()[i]->mask))
00388 {
00389 ExceptionDelCallback::DoDel(source, i);
00390 source.Reply(_("\002%s\002 deleted from session-limit exception list."), mask.c_str());
00391 break;
00392 }
00393 if (i == end)
00394 source.Reply(_("\002%s\002 not found on session-limit exception list."), mask.c_str());
00395 }
00396
00397 if (Anope::ReadOnly)
00398 source.Reply(READ_ONLY_MODE);
00399
00400 return;
00401 }
00402
00403 void DoMove(CommandSource &source, const std::vector<Anope::string> ¶ms)
00404 {
00405 const Anope::string &n1str = params.size() > 1 ? params[1] : "";
00406 const Anope::string &n2str = params.size() > 2 ? params[2] : "";
00407 int n1, n2;
00408
00409 if (n2str.empty())
00410 {
00411 this->OnSyntaxError(source, "MOVE");
00412 return;
00413 }
00414
00415 n1 = n2 = -1;
00416 try
00417 {
00418 n1 = convertTo<int>(n1str);
00419 n2 = convertTo<int>(n2str);
00420 }
00421 catch (const ConvertException &) { }
00422
00423 if (n1 >= 0 && static_cast<unsigned>(n1) < session_service->GetExceptions().size() && n2 >= 0 && static_cast<unsigned>(n2) < session_service->GetExceptions().size() && n1 != n2)
00424 {
00425 Exception *temp = session_service->GetExceptions()[n1];
00426 session_service->GetExceptions()[n1] = session_service->GetExceptions()[n2];
00427 session_service->GetExceptions()[n2] = temp;
00428
00429 source.Reply(_("Exception for \002%s\002 (#%d) moved to position \002%d\002."), session_service->GetExceptions()[n1]->mask.c_str(), n1 + 1, n2 + 1);
00430
00431 if (Anope::ReadOnly)
00432 source.Reply(READ_ONLY_MODE);
00433 }
00434 else
00435 this->OnSyntaxError(source, "MOVE");
00436
00437 return;
00438 }
00439
00440 void ProcessList(CommandSource &source, const std::vector<Anope::string> ¶ms, ListFormatter &list)
00441 {
00442 const Anope::string &mask = params.size() > 1 ? params[1] : "";
00443
00444 if (session_service->GetExceptions().empty())
00445 {
00446 source.Reply(_("The session exception list is empty."));
00447 return;
00448 }
00449
00450 if (!mask.empty() && mask.find_first_not_of("1234567890,-") == Anope::string::npos)
00451 {
00452 class ExceptionListCallback : public NumberList
00453 {
00454 ListFormatter &list;
00455 public:
00456 ExceptionListCallback(ListFormatter &_list, const Anope::string &numlist) : NumberList(numlist, false), list(_list)
00457 {
00458 }
00459
00460 void HandleNumber(unsigned Number) anope_override
00461 {
00462 if (!Number || Number > session_service->GetExceptions().size())
00463 return;
00464
00465 Exception *e = session_service->GetExceptions()[Number - 1];
00466
00467 ListFormatter::ListEntry entry;
00468 entry["Number"] = stringify(Number);
00469 entry["Mask"] = e->mask;
00470 entry["By"] = e->who;
00471 entry["Created"] = Anope::strftime(e->time);
00472 entry["Limit"] = stringify(e->limit);
00473 entry["Reason"] = e->reason;
00474 this->list.AddEntry(entry);
00475 }
00476 }
00477 nl_list(list, mask);
00478 nl_list.Process();
00479 }
00480 else
00481 {
00482 for (unsigned i = 0, end = session_service->GetExceptions().size(); i < end; ++i)
00483 {
00484 Exception *e = session_service->GetExceptions()[i];
00485 if (mask.empty() || Anope::Match(e->mask, mask))
00486 {
00487 ListFormatter::ListEntry entry;
00488 entry["Number"] = stringify(i + 1);
00489 entry["Mask"] = e->mask;
00490 entry["By"] = e->who;
00491 entry["Created"] = Anope::strftime(e->time);
00492 entry["Limit"] = stringify(e->limit);
00493 entry["Reason"] = e->reason;
00494 list.AddEntry(entry);
00495 }
00496 }
00497 }
00498
00499 if (list.IsEmpty())
00500 source.Reply(_("No matching entries on session-limit exception list."));
00501 else
00502 {
00503 source.Reply(_("Current Session Limit Exception list:"));
00504
00505 std::vector<Anope::string> replies;
00506 list.Process(replies);
00507
00508 for (unsigned i = 0; i < replies.size(); ++i)
00509 source.Reply(replies[i]);
00510 }
00511 }
00512
00513 void DoList(CommandSource &source, const std::vector<Anope::string> ¶ms)
00514 {
00515 ListFormatter list;
00516 list.AddColumn("Number").AddColumn("Limit").AddColumn("Mask");
00517
00518 this->ProcessList(source, params, list);
00519 }
00520
00521 void DoView(CommandSource &source, const std::vector<Anope::string> ¶ms)
00522 {
00523 ListFormatter list;
00524 list.AddColumn("Number").AddColumn("Mask").AddColumn("By").AddColumn("Created").AddColumn("Limit").AddColumn("Reason");
00525
00526 this->ProcessList(source, params, list);
00527 }
00528
00529 public:
00530 CommandOSException(Module *creator) : Command(creator, "operserv/exception", 1, 5)
00531 {
00532 this->SetDesc(_("Modify the session-limit exception list"));
00533 this->SetSyntax(_("ADD [\037+expiry\037] \037mask\037 \037limit\037 \037reason\037"));
00534 this->SetSyntax(_("DEL {\037mask\037 | \037list\037}"));
00535 this->SetSyntax(_("MOVE \037num\037 \037position\037"));
00536 this->SetSyntax(_("LIST [\037mask\037 | \037list\037]"));
00537 this->SetSyntax(_("VIEW [\037mask\037 | \037list\037]"));
00538 }
00539
00540 void Execute(CommandSource &source, const std::vector<Anope::string> ¶ms) anope_override
00541 {
00542 const Anope::string &cmd = params[0];
00543
00544 if (!Config->LimitSessions)
00545 {
00546 source.Reply(_("Session limiting is disabled."));
00547 return;
00548 }
00549
00550 if (cmd.equals_ci("ADD"))
00551 return this->DoAdd(source, params);
00552 else if (cmd.equals_ci("DEL"))
00553 return this->DoDel(source, params);
00554 else if (cmd.equals_ci("MOVE"))
00555 return this->DoMove(source, params);
00556 else if (cmd.equals_ci("LIST"))
00557 return this->DoList(source, params);
00558 else if (cmd.equals_ci("VIEW"))
00559 return this->DoView(source, params);
00560 else
00561 this->OnSyntaxError(source, "");
00562
00563 return;
00564 }
00565
00566 bool OnHelp(CommandSource &source, const Anope::string &subcommand) anope_override
00567 {
00568 this->SendSyntax(source);
00569 source.Reply(" ");
00570 source.Reply(_("Allows Services Operators to manipulate the list of hosts that\n"
00571 "have specific session limits - allowing certain machines,\n"
00572 "such as shell servers, to carry more than the default number\n"
00573 "of clients at a time. Once a host reaches its session limit,\n"
00574 "all clients attempting to connect from that host will be\n"
00575 "killed. Before the user is killed, they are notified, via a\n"
00576 "/NOTICE from %s, of a source of help regarding session\n"
00577 "limiting. The content of this notice is a config setting.\n"),
00578 Config->OperServ.c_str());
00579 source.Reply(" ");
00580 source.Reply(_("\002EXCEPTION ADD\002 adds the given host mask to the exception list.\n"
00581 "Note that \002nick!user@host\002 and \002user@host\002 masks are invalid!\n"
00582 "Only real host masks, such as \002box.host.dom\002 and \002*.host.dom\002,\n"
00583 "are allowed because sessions limiting does not take nick or\n"
00584 "user names into account. \037limit\037 must be a number greater than\n"
00585 "or equal to zero. This determines how many sessions this host\n"
00586 "may carry at a time. A value of zero means the host has an\n"
00587 "unlimited session limit. See the \002AKILL\002 help for details about\n"
00588 "the format of the optional \037expiry\037 parameter.\n"
00589 "\002EXCEPTION DEL\002 removes the given mask from the exception list.\n"
00590 "\002EXCEPTION MOVE\002 moves exception \037num\037 to \037position\037. The\n"
00591 "sessions inbetween will be shifted up or down to fill the gap.\n"
00592 "\002EXCEPTION LIST\002 and \002EXCEPTION VIEW\002 show all current\n"
00593 "sessions if the optional mask is given, the list is limited\n"
00594 "to those sessions matching the mask. The difference is that\n"
00595 "\002EXCEPTION VIEW\002 is more verbose, displaying the name of the\n"
00596 "person who added the exception, its session limit, reason,\n"
00597 "host mask and the expiry date and time.\n"
00598 " \n"
00599 "Note that a connecting client will \"use\" the first exception\n"
00600 "their host matches."));
00601 return true;
00602 }
00603 };
00604
00605 class OSSession : public Module
00606 {
00607 Serialize::Type exception_type;
00608 MySessionService ss;
00609 ExpireTimer expiretimer;
00610 CommandOSSession commandossession;
00611 CommandOSException commandosexception;
00612 ServiceReference<XLineManager> akills;
00613
00614 void AddSession(User *u, bool exempt)
00615 {
00616 Session *session;
00617 try
00618 {
00619 session = this->ss.FindSession(u->ip);
00620 }
00621 catch (const SocketException &)
00622 {
00623 return;
00624 }
00625
00626 if (session)
00627 {
00628 bool kill = false;
00629 if (Config->DefSessionLimit && session->count >= Config->DefSessionLimit)
00630 {
00631 kill = true;
00632 Exception *exception = this->ss.FindException(u);
00633 if (exception)
00634 {
00635 kill = false;
00636 if (exception->limit && session->count >= exception->limit)
00637 kill = true;
00638 }
00639 }
00640
00641
00642
00643
00644
00645
00646
00647
00648 ++session->count;
00649
00650 if (kill && !exempt)
00651 {
00652 if (OperServ)
00653 {
00654 if (!Config->SessionLimitExceeded.empty())
00655 u->SendMessage(OperServ, Config->SessionLimitExceeded.c_str(), u->ip.c_str());
00656 if (!Config->SessionLimitDetailsLoc.empty())
00657 u->SendMessage(OperServ, "%s", Config->SessionLimitDetailsLoc.c_str());
00658 }
00659
00660 ++session->hits;
00661 if (Config->MaxSessionKill && session->hits >= Config->MaxSessionKill && akills)
00662 {
00663 const Anope::string &akillmask = "*@" + u->ip;
00664 XLine *x = new XLine(akillmask, Config->OperServ, Anope::CurTime + Config->SessionAutoKillExpiry, "Session limit exceeded", XLineManager::GenerateUID());
00665 akills->AddXLine(x);
00666 akills->Send(NULL, x);
00667 Log(OperServ, "akill/session") << "Added a temporary AKILL for \002" << akillmask << "\002 due to excessive connections";
00668 }
00669 else
00670 {
00671 u->Kill(Config->OperServ, "Session limit exceeded");
00672 u = NULL;
00673 }
00674 }
00675 }
00676 else
00677 {
00678 session = new Session(u->ip, u->ip.find(':') != Anope::string::npos ? Config->SessionIPv6CIDR : Config->SessionIPv4CIDR);
00679 this->ss.AddSession(session);
00680 }
00681 }
00682
00683 void DelSession(User *u)
00684 {
00685 Session *session;
00686 try
00687 {
00688 session = this->ss.FindSession(u->ip);
00689 }
00690 catch (const SocketException &)
00691 {
00692 return;
00693 }
00694 if (!session)
00695 {
00696 Log(LOG_DEBUG) << "Tried to delete non-existant session: " << u->ip;
00697 return;
00698 }
00699
00700 if (session->count > 1)
00701 {
00702 --session->count;
00703 return;
00704 }
00705
00706 this->ss.DelSession(session);
00707 delete session;
00708 }
00709
00710 public:
00711 OSSession(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, CORE),
00712 exception_type("Exception", Exception::Unserialize), ss(this), commandossession(this), commandosexception(this), akills("XLineManager", "xlinemanager/sgline")
00713 {
00714 this->SetAuthor("Anope");
00715 this->SetPermanent(true);
00716
00717 Implementation i[] = { I_OnUserConnect, I_OnPreUserLogoff };
00718 ModuleManager::Attach(i, this, sizeof(i) / sizeof(Implementation));
00719 ModuleManager::SetPriority(this, PRIORITY_FIRST);
00720 }
00721
00722 void OnUserConnect(User *user, bool &exempt) anope_override
00723 {
00724 if (!user->Quitting() && Config->LimitSessions)
00725 this->AddSession(user, exempt);
00726 }
00727
00728 void OnPreUserLogoff(User *u) anope_override
00729 {
00730 if (Config->LimitSessions && (!u->server || !u->server->IsULined()))
00731 this->DelSession(u);
00732 }
00733 };
00734
00735 MODULE_INIT(OSSession)