1 /* OperServ core functions
2  *
3  * (C) 2003-2020 Anope Team
4  * Contact us at team@anope.org
5  *
6  * Please read COPYING and README for further details.
7  *
8  * Based on the original code of Epona by Lara.
9  * Based on the original code of Services by Andy Church.
10  */
11 
12 #include "module.h"
13 #include "modules/os_session.h"
14 
15 namespace
16 {
17 	/* The default session limit */
18 	unsigned session_limit;
19 	/* How many times to kill before adding an AKILL */
20 	unsigned max_session_kill;
21 	/* How long session akills should last */
22 	time_t session_autokill_expiry;
23 	/* Reason to use for session kills */
24 	Anope::string sle_reason;
25 	/* Optional second reason */
26 	Anope::string sle_detailsloc;
27 
28 	/* Max limit that can be used for exceptions */
29 	unsigned max_exception_limit;
30 	/* How long before exceptions expire by default */
31 	time_t exception_expiry;
32 
33 	/* Number of bits to use when comparing session IPs */
34 	unsigned ipv4_cidr;
35 	unsigned ipv6_cidr;
36 }
37 
38 class MySessionService : public SessionService
39 {
40 	SessionMap Sessions;
41 	Serialize::Checker<ExceptionVector> Exceptions;
42  public:
MySessionService(Module * m)43 	MySessionService(Module *m) : SessionService(m), Exceptions("Exception") { }
44 
CreateException()45 	Exception *CreateException() anope_override
46 	{
47 		return new Exception();
48 	}
49 
AddException(Exception * e)50 	void AddException(Exception *e) anope_override
51 	{
52 		this->Exceptions->push_back(e);
53 	}
54 
DelException(Exception * e)55 	void DelException(Exception *e) anope_override
56 	{
57 		ExceptionVector::iterator it = std::find(this->Exceptions->begin(), this->Exceptions->end(), e);
58 		if (it != this->Exceptions->end())
59 			this->Exceptions->erase(it);
60 	}
61 
FindException(User * u)62 	Exception *FindException(User *u) anope_override
63 	{
64 		for (std::vector<Exception *>::const_iterator it = this->Exceptions->begin(), it_end = this->Exceptions->end(); it != it_end; ++it)
65 		{
66 			Exception *e = *it;
67 			if (Anope::Match(u->host, e->mask) || Anope::Match(u->ip.addr(), e->mask))
68 				return e;
69 
70 			if (cidr(e->mask).match(u->ip))
71 				return e;
72 		}
73 		return NULL;
74 	}
75 
FindException(const Anope::string & host)76 	Exception *FindException(const Anope::string &host) anope_override
77 	{
78 		for (std::vector<Exception *>::const_iterator it = this->Exceptions->begin(), it_end = this->Exceptions->end(); it != it_end; ++it)
79 		{
80 			Exception *e = *it;
81 			if (Anope::Match(host, e->mask))
82 				return e;
83 
84 			if (cidr(e->mask).match(sockaddrs(host)))
85 				return e;
86 		}
87 
88 		return NULL;
89 	}
90 
GetExceptions()91 	ExceptionVector &GetExceptions() anope_override
92 	{
93 		return this->Exceptions;
94 	}
95 
DelSession(Session * s)96 	void DelSession(Session *s)
97 	{
98 		this->Sessions.erase(s->addr);
99 	}
100 
FindSession(const Anope::string & ip)101 	Session *FindSession(const Anope::string &ip) anope_override
102 	{
103 		cidr c(ip, ip.find(':') != Anope::string::npos ? ipv6_cidr : ipv4_cidr);
104 		if (!c.valid())
105 			return NULL;
106 		SessionMap::iterator it = this->Sessions.find(c);
107 		if (it != this->Sessions.end())
108 			return it->second;
109 		return NULL;
110 	}
111 
FindSessionIterator(const sockaddrs & ip)112 	SessionMap::iterator FindSessionIterator(const sockaddrs &ip)
113 	{
114 		cidr c(ip, ip.ipv6() ? ipv6_cidr : ipv4_cidr);
115 		if (!c.valid())
116 			return this->Sessions.end();
117 		return this->Sessions.find(c);
118 	}
119 
FindOrCreateSession(const cidr & ip)120 	Session* &FindOrCreateSession(const cidr &ip)
121 	{
122 		return this->Sessions[ip];
123 	}
124 
GetSessions()125 	SessionMap &GetSessions() anope_override
126 	{
127 		return this->Sessions;
128 	}
129 };
130 
131 class ExceptionDelCallback : public NumberList
132 {
133  protected:
134 	CommandSource &source;
135 	unsigned deleted;
136 	Command *cmd;
137  public:
ExceptionDelCallback(CommandSource & _source,const Anope::string & numlist,Command * c)138 	ExceptionDelCallback(CommandSource &_source, const Anope::string &numlist, Command *c) : NumberList(numlist, true), source(_source), deleted(0), cmd(c)
139 	{
140 	}
141 
~ExceptionDelCallback()142 	~ExceptionDelCallback()
143 	{
144 		if (!deleted)
145 			source.Reply(_("No matching entries on session-limit exception list."));
146 		else if (deleted == 1)
147 			source.Reply(_("Deleted 1 entry from session-limit exception list."));
148 		else
149 			source.Reply(_("Deleted %d entries from session-limit exception list."), deleted);
150 	}
151 
HandleNumber(unsigned number)152 	virtual void HandleNumber(unsigned number) anope_override
153 	{
154 		if (!number || number > session_service->GetExceptions().size())
155 			return;
156 
157 		Log(LOG_ADMIN, source, cmd) << "to remove the session limit exception for " << session_service->GetExceptions()[number - 1]->mask;
158 
159 		++deleted;
160 		DoDel(source, number - 1);
161 	}
162 
DoDel(CommandSource & source,unsigned index)163 	static void DoDel(CommandSource &source, unsigned index)
164 	{
165 		Exception *e = session_service->GetExceptions()[index];
166 		FOREACH_MOD(OnExceptionDel, (source, e));
167 
168 		session_service->DelException(e);
169 		delete e;
170 	}
171 };
172 
173 class CommandOSSession : public Command
174 {
175  private:
DoList(CommandSource & source,const std::vector<Anope::string> & params)176 	void DoList(CommandSource &source, const std::vector<Anope::string> &params)
177 	{
178 		Anope::string param = params[1];
179 
180 		unsigned mincount = 0;
181 		try
182 		{
183 			mincount = convertTo<unsigned>(param);
184 		}
185 		catch (const ConvertException &) { }
186 
187 		if (mincount <= 1)
188 			source.Reply(_("Invalid threshold value. It must be a valid integer greater than 1."));
189 		else
190 		{
191 			ListFormatter list(source.GetAccount());
192 			list.AddColumn(_("Session")).AddColumn(_("Host"));
193 
194 			for (SessionService::SessionMap::iterator it = session_service->GetSessions().begin(), it_end = session_service->GetSessions().end(); it != it_end; ++it)
195 			{
196 				Session *session = it->second;
197 
198 				if (session->count >= mincount)
199 				{
200 					ListFormatter::ListEntry entry;
201 					entry["Session"] = stringify(session->count);
202 					entry["Host"] = session->addr.mask();
203 					list.AddEntry(entry);
204 				}
205 			}
206 
207 			source.Reply(_("Hosts with at least \002%d\002 sessions:"), mincount);
208 
209 			std::vector<Anope::string> replies;
210 			list.Process(replies);
211 
212 
213 			for (unsigned i = 0; i < replies.size(); ++i)
214 				source.Reply(replies[i]);
215 		}
216 
217 		return;
218 	}
219 
DoView(CommandSource & source,const std::vector<Anope::string> & params)220 	void DoView(CommandSource &source, const std::vector<Anope::string> &params)
221 	{
222 		Anope::string param = params[1];
223 		Session *session = session_service->FindSession(param);
224 
225 		Exception *exception = session_service->FindException(param);
226 		Anope::string entry = "no entry";
227 		unsigned limit = session_limit;
228 		if (exception)
229 		{
230 			if (!exception->limit)
231 				limit = 0;
232 			else if (exception->limit > limit)
233 				limit = exception->limit;
234 			entry = exception->mask;
235 		}
236 
237 		if (!session)
238 			source.Reply(_("\002%s\002 not found on session list, but has a limit of \002%d\002 because it matches entry: \002%s\002."), param.c_str(), limit, entry.c_str());
239 		else
240 			source.Reply(_("The host \002%s\002 currently has \002%d\002 sessions with a limit of \002%d\002 because it matches entry: \002%s\002."), session->addr.mask().c_str(), session->count, limit, entry.c_str());
241 	}
242  public:
CommandOSSession(Module * creator)243 	CommandOSSession(Module *creator) : Command(creator, "operserv/session", 2, 2)
244 	{
245 		this->SetDesc(_("View the list of host sessions"));
246 		this->SetSyntax(_("LIST \037threshold\037"));
247 		this->SetSyntax(_("VIEW \037host\037"));
248 	}
249 
Execute(CommandSource & source,const std::vector<Anope::string> & params)250 	void Execute(CommandSource &source, const std::vector<Anope::string> &params) anope_override
251 	{
252 		const Anope::string &cmd = params[0];
253 
254 		Log(LOG_ADMIN, source, this) << cmd << " " << params[1];
255 
256 		if (!session_limit)
257 			source.Reply(_("Session limiting is disabled."));
258 		else if (cmd.equals_ci("LIST"))
259 			return this->DoList(source, params);
260 		else if (cmd.equals_ci("VIEW"))
261 			return this->DoView(source, params);
262 		else
263 			this->OnSyntaxError(source, "");
264 	}
265 
OnHelp(CommandSource & source,const Anope::string & subcommand)266 	bool OnHelp(CommandSource &source, const Anope::string &subcommand) anope_override
267 	{
268 		this->SendSyntax(source);
269 		source.Reply(" ");
270 		source.Reply(_("Allows Services Operators to view the session list.\n"
271 				" \n"
272 				"\002SESSION LIST\002 lists hosts with at least \037threshold\037 sessions.\n"
273 				"The threshold must be a number greater than 1. This is to\n"
274 				"prevent accidental listing of the large number of single\n"
275 				"session hosts.\n"
276 				" \n"
277 				"\002SESSION VIEW\002 displays detailed information about a specific\n"
278 				"host - including the current session count and session limit.\n"
279 				"The \037host\037 value may not include wildcards.\n"
280 				" \n"
281 				"See the \002EXCEPTION\002 help for more information about session\n"
282 				"limiting and how to set session limits specific to certain\n"
283 				"hosts and groups thereof."));
284 		return true;
285 	}
286 };
287 
288 class CommandOSException : public Command
289 {
290  private:
DoAdd(CommandSource & source,const std::vector<Anope::string> & params)291 	void DoAdd(CommandSource &source, const std::vector<Anope::string> &params)
292 	{
293 		Anope::string mask, expiry, limitstr;
294 		unsigned last_param = 3;
295 
296 		mask = params.size() > 1 ? params[1] : "";
297 		if (!mask.empty() && mask[0] == '+')
298 		{
299 			expiry = mask;
300 			mask = params.size() > 2 ? params[2] : "";
301 			last_param = 4;
302 		}
303 
304 		limitstr = params.size() > last_param - 1 ? params[last_param - 1] : "";
305 
306 		if (params.size() <= last_param)
307 		{
308 			this->OnSyntaxError(source, "ADD");
309 			return;
310 		}
311 
312 		Anope::string reason = params[last_param];
313 		if (last_param == 3 && params.size() > 4)
314 			reason += " " + params[4];
315 		if (reason.empty())
316 		{
317 			this->OnSyntaxError(source, "ADD");
318 			return;
319 		}
320 
321 		time_t expires = !expiry.empty() ? Anope::DoTime(expiry) : exception_expiry;
322 		if (expires < 0)
323 		{
324 			source.Reply(BAD_EXPIRY_TIME);
325 			return;
326 		}
327 		else if (expires > 0)
328 			expires += Anope::CurTime;
329 
330 		unsigned limit = -1;
331 		try
332 		{
333 			limit = convertTo<unsigned>(limitstr);
334 		}
335 		catch (const ConvertException &) { }
336 
337 		if (limit > max_exception_limit)
338 		{
339 			source.Reply(_("Invalid session limit. It must be a valid integer greater than or equal to zero and less than \002%d\002."), max_exception_limit);
340 			return;
341 		}
342 		else
343 		{
344 			if (mask.find('!') != Anope::string::npos || mask.find('@') != Anope::string::npos)
345 			{
346 				source.Reply(_("Invalid hostmask. Only real hostmasks are valid, as exceptions are not matched against nicks or usernames."));
347 				return;
348 			}
349 
350 			for (std::vector<Exception *>::iterator it = session_service->GetExceptions().begin(), it_end = session_service->GetExceptions().end(); it != it_end; ++it)
351 			{
352 				Exception *e = *it;
353 				if (e->mask.equals_ci(mask))
354 				{
355 					if (e->limit != limit)
356 					{
357 						e->limit = limit;
358 						source.Reply(_("Exception for \002%s\002 has been updated to %d."), mask.c_str(), e->limit);
359 					}
360 					else
361 						source.Reply(_("\002%s\002 already exists on the EXCEPTION list."), mask.c_str());
362 					return;
363 				}
364 			}
365 
366 			Exception *exception = new Exception();
367 			exception->mask = mask;
368 			exception->limit = limit;
369 			exception->reason = reason;
370 			exception->time = Anope::CurTime;
371 			exception->who = source.GetNick();
372 			exception->expires = expires;
373 
374 			EventReturn MOD_RESULT;
375 			FOREACH_RESULT(OnExceptionAdd, MOD_RESULT, (exception));
376 			if (MOD_RESULT == EVENT_STOP)
377 				delete exception;
378 			else
379 			{
380 				Log(LOG_ADMIN, source, this) << "to set the session limit for " << mask << " to " << limit;
381 				session_service->AddException(exception);
382 				source.Reply(_("Session limit for \002%s\002 set to \002%d\002."), mask.c_str(), limit);
383 				if (Anope::ReadOnly)
384 					source.Reply(READ_ONLY_MODE);
385 			}
386 		}
387 
388 		return;
389 	}
390 
DoDel(CommandSource & source,const std::vector<Anope::string> & params)391 	void DoDel(CommandSource &source, const std::vector<Anope::string> &params)
392 	{
393 		const Anope::string &mask = params.size() > 1 ? params[1] : "";
394 
395 		if (mask.empty())
396 		{
397 			this->OnSyntaxError(source, "DEL");
398 			return;
399 		}
400 
401 		if (isdigit(mask[0]) && mask.find_first_not_of("1234567890,-") == Anope::string::npos)
402 		{
403 			ExceptionDelCallback list(source, mask, this);
404 			list.Process();
405 		}
406 		else
407 		{
408 			unsigned i = 0, end = session_service->GetExceptions().size();
409 			for (; i < end; ++i)
410 				if (mask.equals_ci(session_service->GetExceptions()[i]->mask))
411 				{
412 					Log(LOG_ADMIN, source, this) << "to remove the session limit exception for " << mask;
413 					ExceptionDelCallback::DoDel(source, i);
414 					source.Reply(_("\002%s\002 deleted from session-limit exception list."), mask.c_str());
415 					break;
416 				}
417 			if (i == end)
418 				source.Reply(_("\002%s\002 not found on session-limit exception list."), mask.c_str());
419 		}
420 
421 		if (Anope::ReadOnly)
422 			source.Reply(READ_ONLY_MODE);
423 
424 		return;
425 	}
426 
ProcessList(CommandSource & source,const std::vector<Anope::string> & params,ListFormatter & list)427 	void ProcessList(CommandSource &source, const std::vector<Anope::string> &params, ListFormatter &list)
428 	{
429 		const Anope::string &mask = params.size() > 1 ? params[1] : "";
430 
431 		if (session_service->GetExceptions().empty())
432 		{
433 			source.Reply(_("The session exception list is empty."));
434 			return;
435 		}
436 
437 		if (!mask.empty() && mask.find_first_not_of("1234567890,-") == Anope::string::npos)
438 		{
439 			class ExceptionListCallback : public NumberList
440 			{
441 				CommandSource &source;
442 				ListFormatter &list;
443 			 public:
444 				ExceptionListCallback(CommandSource &_source, ListFormatter &_list, const Anope::string &numlist) : NumberList(numlist, false), source(_source), list(_list)
445 				{
446 				}
447 
448 				void HandleNumber(unsigned Number) anope_override
449 				{
450 					if (!Number || Number > session_service->GetExceptions().size())
451 						return;
452 
453 					Exception *e = session_service->GetExceptions()[Number - 1];
454 
455 					ListFormatter::ListEntry entry;
456 					entry["Number"] = stringify(Number);
457 					entry["Mask"] = e->mask;
458 					entry["By"] = e->who;
459 					entry["Created"] = Anope::strftime(e->time, NULL, true);
460 					entry["Expires"] = Anope::Expires(e->expires, source.GetAccount());
461 					entry["Limit"] = stringify(e->limit);
462 					entry["Reason"] = e->reason;
463 					this->list.AddEntry(entry);
464 				}
465 			}
466 			nl_list(source, list, mask);
467 			nl_list.Process();
468 		}
469 		else
470 		{
471 			for (unsigned i = 0, end = session_service->GetExceptions().size(); i < end; ++i)
472 			{
473 				Exception *e = session_service->GetExceptions()[i];
474 				if (mask.empty() || Anope::Match(e->mask, mask))
475 				{
476 					ListFormatter::ListEntry entry;
477 					entry["Number"] = stringify(i + 1);
478 					entry["Mask"] = e->mask;
479 					entry["By"] = e->who;
480 					entry["Created"] = Anope::strftime(e->time, NULL, true);
481 					entry["Expires"] = Anope::Expires(e->expires, source.GetAccount());
482 					entry["Limit"] = stringify(e->limit);
483 					entry["Reason"] = e->reason;
484 					list.AddEntry(entry);
485 				}
486 			}
487 		}
488 
489 		if (list.IsEmpty())
490 			source.Reply(_("No matching entries on session-limit exception list."));
491 		else
492 		{
493 			source.Reply(_("Current Session Limit Exception list:"));
494 
495 			std::vector<Anope::string> replies;
496 			list.Process(replies);
497 
498 			for (unsigned i = 0; i < replies.size(); ++i)
499 				source.Reply(replies[i]);
500 		}
501 	}
502 
DoList(CommandSource & source,const std::vector<Anope::string> & params)503 	void DoList(CommandSource &source, const std::vector<Anope::string> &params)
504 	{
505 		ListFormatter list(source.GetAccount());
506 		list.AddColumn(_("Number")).AddColumn(_("Limit")).AddColumn(_("Mask"));
507 
508 		this->ProcessList(source, params, list);
509 	}
510 
DoView(CommandSource & source,const std::vector<Anope::string> & params)511 	void DoView(CommandSource &source, const std::vector<Anope::string> &params)
512 	{
513 		ListFormatter list(source.GetAccount());
514 		list.AddColumn(_("Number")).AddColumn(_("Mask")).AddColumn(_("By")).AddColumn(_("Created")).AddColumn(_("Expires")).AddColumn(_("Limit")).AddColumn(_("Reason"));
515 
516 		this->ProcessList(source, params, list);
517 	}
518 
519  public:
CommandOSException(Module * creator)520 	CommandOSException(Module *creator) : Command(creator, "operserv/exception", 1, 5)
521 	{
522 		this->SetDesc(_("Modify the session-limit exception list"));
523 		this->SetSyntax(_("ADD [\037+expiry\037] \037mask\037 \037limit\037 \037reason\037"));
524 		this->SetSyntax(_("DEL {\037mask\037 | \037entry-num\037 | \037list\037}"));
525 		this->SetSyntax(_("LIST [\037mask\037 | \037list\037]"));
526 		this->SetSyntax(_("VIEW [\037mask\037 | \037list\037]"));
527 	}
528 
Execute(CommandSource & source,const std::vector<Anope::string> & params)529 	void Execute(CommandSource &source, const std::vector<Anope::string> &params) anope_override
530 	{
531 		const Anope::string &cmd = params[0];
532 
533 		if (!session_limit)
534 			source.Reply(_("Session limiting is disabled."));
535 		else if (cmd.equals_ci("ADD"))
536 			return this->DoAdd(source, params);
537 		else if (cmd.equals_ci("DEL"))
538 			return this->DoDel(source, params);
539 		else if (cmd.equals_ci("LIST"))
540 			return this->DoList(source, params);
541 		else if (cmd.equals_ci("VIEW"))
542 			return this->DoView(source, params);
543 		else
544 			this->OnSyntaxError(source, "");
545 	}
546 
OnHelp(CommandSource & source,const Anope::string & subcommand)547 	bool OnHelp(CommandSource &source, const Anope::string &subcommand) anope_override
548 	{
549 		this->SendSyntax(source);
550 		source.Reply(" ");
551 		source.Reply(_("Allows Services Operators to manipulate the list of hosts that\n"
552 				"have specific session limits - allowing certain machines,\n"
553 				"such as shell servers, to carry more than the default number\n"
554 				"of clients at a time. Once a host reaches its session limit,\n"
555 				"all clients attempting to connect from that host will be\n"
556 				"killed. Before the user is killed, they are notified, of a\n"
557 				"source of help regarding session limiting. The content of\n"
558 				"this notice is a config setting."));
559 		source.Reply(" ");
560 		source.Reply(_("\002EXCEPTION ADD\002 adds the given host mask to the exception list.\n"
561 				"Note that \002nick!user@host\002 and \002user@host\002 masks are invalid!\n"
562 				"Only real host masks, such as \002box.host.dom\002 and \002*.host.dom\002,\n"
563 				"are allowed because sessions limiting does not take nick or\n"
564 				"user names into account. \037limit\037 must be a number greater than\n"
565 				"or equal to zero. This determines how many sessions this host\n"
566 				"may carry at a time. A value of zero means the host has an\n"
567 				"unlimited session limit. See the \002AKILL\002 help for details about\n"
568 				"the format of the optional \037expiry\037 parameter.\n"
569 				" \n"
570 				"\002EXCEPTION DEL\002 removes the given mask from the exception list.\n"
571 				" \n"
572 				"\002EXCEPTION LIST\002 and \002EXCEPTION VIEW\002 show all current\n"
573 				"sessions if the optional mask is given, the list is limited\n"
574 				"to those sessions matching the mask. The difference is that\n"
575 				"\002EXCEPTION VIEW\002 is more verbose, displaying the name of the\n"
576 				"person who added the exception, its session limit, reason,\n"
577 				"host mask and the expiry date and time.\n"
578 				" \n"
579 				"Note that a connecting client will \"use\" the first exception\n"
580 				"their host matches."));
581 		return true;
582 	}
583 };
584 
585 class OSSession : public Module
586 {
587 	Serialize::Type exception_type;
588 	MySessionService ss;
589 	CommandOSSession commandossession;
590 	CommandOSException commandosexception;
591 	ServiceReference<XLineManager> akills;
592 
593  public:
OSSession(const Anope::string & modname,const Anope::string & creator)594 	OSSession(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, VENDOR),
595 		exception_type("Exception", Exception::Unserialize), ss(this), commandossession(this), commandosexception(this), akills("XLineManager", "xlinemanager/sgline")
596 	{
597 		this->SetPermanent(true);
598 	}
599 
Prioritize()600 	void Prioritize() anope_override
601 	{
602 		ModuleManager::SetPriority(this, PRIORITY_FIRST);
603 	}
604 
OnReload(Configuration::Conf * conf)605 	void OnReload(Configuration::Conf *conf) anope_override
606 	{
607 		Configuration::Block *block = Config->GetModule(this);
608 
609 		session_limit = block->Get<int>("defaultsessionlimit");
610 		max_session_kill = block->Get<int>("maxsessionkill");
611 		session_autokill_expiry = block->Get<time_t>("sessionautokillexpiry");
612 		sle_reason = block->Get<const Anope::string>("sessionlimitexceeded");
613 		sle_detailsloc = block->Get<const Anope::string>("sessionlimitdetailsloc");
614 
615 		max_exception_limit = block->Get<int>("maxsessionlimit");
616 		exception_expiry = block->Get<time_t>("exceptionexpiry");
617 
618 		ipv4_cidr = block->Get<unsigned>("session_ipv4_cidr", "32");
619 		ipv6_cidr = block->Get<unsigned>("session_ipv6_cidr", "128");
620 
621 		if (ipv4_cidr > 32 || ipv6_cidr > 128)
622 			throw ConfigException(this->name + ": session CIDR value out of range");
623 	}
624 
OnUserConnect(User * u,bool & exempt)625 	void OnUserConnect(User *u, bool &exempt) anope_override
626 	{
627 		if (u->Quitting() || !session_limit || exempt || !u->server || u->server->IsULined())
628 			return;
629 
630 		cidr u_ip(u->ip, u->ip.ipv6() ? ipv6_cidr : ipv4_cidr);
631 		if (!u_ip.valid())
632 			return;
633 
634 		Session* &session = this->ss.FindOrCreateSession(u_ip);
635 
636 		if (session)
637 		{
638 			bool kill = false;
639 			if (session->count >= session_limit)
640 			{
641 				kill = true;
642 				Exception *exception = this->ss.FindException(u);
643 				if (exception)
644 				{
645 					kill = false;
646 					if (exception->limit && session->count >= exception->limit)
647 						kill = true;
648 				}
649 			}
650 
651 			/* Previously on IRCds that send a QUIT (InspIRCD) when a user is killed, the session for a host was
652 			 * decremented in do_quit, which caused problems and fixed here
653 			 *
654 			 * Now, we create the user struture before calling this to fix some user tracking issues,
655 			 * so we must increment this here no matter what because it will either be
656 			 * decremented when the user is killed or quits - Adam
657 			 */
658 			++session->count;
659 
660 			if (kill && !exempt)
661 			{
662 				BotInfo *OperServ = Config->GetClient("OperServ");
663 				if (OperServ)
664 				{
665 					if (!sle_reason.empty())
666 					{
667 						Anope::string message = sle_reason.replace_all_cs("%IP%", u->ip.addr());
668 						u->SendMessage(OperServ, message);
669 					}
670 					if (!sle_detailsloc.empty())
671 						u->SendMessage(OperServ, sle_detailsloc);
672 				}
673 
674 				++session->hits;
675 
676 				const Anope::string &akillmask = "*@" + session->addr.mask();
677 				if (max_session_kill && session->hits >= max_session_kill && akills && !akills->HasEntry(akillmask))
678 				{
679 					XLine *x = new XLine(akillmask, OperServ ? OperServ->nick : "", Anope::CurTime + session_autokill_expiry, "Session limit exceeded", XLineManager::GenerateUID());
680 					akills->AddXLine(x);
681 					akills->Send(NULL, x);
682 					Log(OperServ, "akill/session") << "Added a temporary AKILL for \002" << akillmask << "\002 due to excessive connections";
683 				}
684 				else
685 				{
686 					u->Kill(OperServ, "Session limit exceeded");
687 				}
688 			}
689 		}
690 		else
691 		{
692 			session = new Session(u->ip, u->ip.ipv6() ? ipv6_cidr : ipv4_cidr);
693 		}
694 	}
695 
OnUserQuit(User * u,const Anope::string & msg)696 	void OnUserQuit(User *u, const Anope::string &msg) anope_override
697 	{
698 		if (!session_limit || !u->server || u->server->IsULined())
699 			return;
700 
701 		SessionService::SessionMap &sessions = this->ss.GetSessions();
702 		SessionService::SessionMap::iterator sit = this->ss.FindSessionIterator(u->ip);
703 
704 		if (sit == sessions.end())
705 			return;
706 
707 		Session *session = sit->second;
708 
709 		if (session->count > 1)
710 		{
711 			--session->count;
712 			return;
713 		}
714 
715 		delete session;
716 		sessions.erase(sit);
717 	}
718 
OnExpireTick()719 	void OnExpireTick() anope_override
720 	{
721 		if (Anope::NoExpire)
722 			return;
723 		for (unsigned i = this->ss.GetExceptions().size(); i > 0; --i)
724 		{
725 			Exception *e = this->ss.GetExceptions()[i - 1];
726 
727 			if (!e->expires || e->expires > Anope::CurTime)
728 				continue;
729 			BotInfo *OperServ = Config->GetClient("OperServ");
730 			Log(OperServ, "expire/exception") << "Session exception for " << e->mask << " has expired.";
731 			this->ss.DelException(e);
732 			delete e;
733 		}
734 	}
735 };
736 
737 MODULE_INIT(OSSession)
738