1 /*****************************************************************************
2  * PokerTH - The open source texas holdem engine                             *
3  * Copyright (C) 2006-2012 Felix Hammer, Florian Thauer, Lothar May          *
4  *                                                                           *
5  * This program is free software: you can redistribute it and/or modify      *
6  * it under the terms of the GNU Affero General Public License as            *
7  * published by the Free Software Foundation, either version 3 of the        *
8  * License, or (at your option) any later version.                           *
9  *                                                                           *
10  * This program is distributed in the hope that it will be useful,           *
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of            *
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the             *
13  * GNU Affero General Public License for more details.                       *
14  *                                                                           *
15  * You should have received a copy of the GNU Affero General Public License  *
16  * along with this program.  If not, see <http://www.gnu.org/licenses/>.     *
17  *                                                                           *
18  *                                                                           *
19  * Additional permission under GNU AGPL version 3 section 7                  *
20  *                                                                           *
21  * If you modify this program, or any covered work, by linking or            *
22  * combining it with the OpenSSL project's OpenSSL library (or a             *
23  * modified version of that library), containing parts covered by the        *
24  * terms of the OpenSSL or SSLeay licenses, the authors of PokerTH           *
25  * (Felix Hammer, Florian Thauer, Lothar May) grant you additional           *
26  * permission to convey the resulting work.                                  *
27  * Corresponding Source for a non-source form of such a combination          *
28  * shall include the source code for the parts of OpenSSL used as well       *
29  * as that of the covered work.                                              *
30  *****************************************************************************/
31 
32 #include <net/socket_helper.h>
33 #include <net/ircthread.h>
34 #include <net/socket_msg.h>
35 #ifdef _WIN32
36 #include <libircclient/libircclient.h>
37 #else
38 #include <libircclient.h>
39 #endif
40 
41 // We need to do the following to handle different versions of libircclient.
42 // Sadly, libircclient doesn't have actual definitions for its versions in its headers.
43 // However, we can use a definition that appeared in the same version we need
44 // to check for. Hacky, but hey, it works.
45 #ifdef LIBIRC_OPTION_SSL_NO_VERIFY
46 #ifdef _WIN32
47 #include <libircclient/libirc_rfcnumeric.h>
48 #else
49 #include <libirc_rfcnumeric.h>
50 #endif
51 #endif
52 
53 #include <boost/algorithm/string/predicate.hpp>
54 #include <queue>
55 #include <sstream>
56 #include <cctype>
57 #include <cstring>
58 
59 using namespace std;
60 
61 #define IRC_WAIT_TERMINATION_MSEC			500
62 #define IRC_RECV_TIMEOUT_MSEC				50
63 #define IRC_MAX_RENAME_TRIES				5
64 #define IRC_MIN_RECONNECT_INTERVAL_SEC		60
65 #define IRC_SEND_LIMIT_BYTES				1024
66 
67 #define IRC_RENAME_ATTACH					"|Lobby"
68 #define IRC_MAX_NICK_LEN					16
69 
70 
71 struct IrcContext {
IrcContextIrcContext72 	IrcContext(IrcThread &t) : ircThread(t), session(NULL), serverPort(0), useIPv6(false), renameTries(0), sendingBlocked(false), sendCounter(0) {}
73 	IrcThread &ircThread;
74 	irc_session_t *session;
75 	string serverAddress;
76 	unsigned serverPort;
77 	bool useIPv6;
78 	string origNick;
79 	string nick;
80 	string channel;
81 	string channelPassword;
82 	unsigned renameTries;
83 	bool sendingBlocked;
84 	size_t sendCounter;
85 	queue<string> sendQueue;
86 };
87 
irc_auto_rename_nick(irc_session_t * session)88 void irc_auto_rename_nick(irc_session_t *session)
89 {
90 	IrcContext *context = static_cast<IrcContext *>(irc_get_ctx(session));
91 
92 	if (context->renameTries <= IRC_MAX_RENAME_TRIES) { // Limit number of rename tries.
93 		// Automatically rename the nick on collision.
94 		// First: Try to append the string "Lobby".
95 		if (context->nick.find(IRC_RENAME_ATTACH) == string::npos) {
96 			if (context->nick.length() + (sizeof(IRC_RENAME_ATTACH)) > IRC_MAX_NICK_LEN)
97 				context->nick = context->nick.substr(0, IRC_MAX_NICK_LEN - (sizeof(IRC_RENAME_ATTACH)));
98 			context->nick = context->nick + IRC_RENAME_ATTACH;
99 		} else {
100 			// This didn't work out. Append a number or increment it.
101 			string::reverse_iterator end = context->nick.rbegin();
102 			if (!context->nick.empty() && isdigit(*end)) {
103 				if (*end != '9')
104 					*end = (*end) + 1;
105 				else
106 					context->nick = context->nick + "0";
107 			} else
108 				context->nick = context->nick + "1";
109 		}
110 		irc_cmd_nick(session, context->nick.c_str());
111 		context->renameTries++;
112 	} else
113 		irc_cmd_quit(session, NULL);
114 }
115 
irc_notify_player_list(irc_session_t * session,const char * players)116 void irc_notify_player_list(irc_session_t *session, const char *players)
117 {
118 	IrcContext *context = static_cast<IrcContext *>(irc_get_ctx(session));
119 
120 	istringstream input(players);
121 	string name;
122 	input >> name;
123 	while (!input.fail() && !input.eof()) {
124 		context->ircThread.GetCallback().SignalIrcPlayerJoined(name);
125 		input >> name;
126 	}
127 }
128 
irc_handle_server_error(irc_session_t * session,unsigned irc_error_code)129 void irc_handle_server_error(irc_session_t *session, unsigned irc_error_code)
130 {
131 	IrcContext *context = static_cast<IrcContext *>(irc_get_ctx(session));
132 
133 	context->ircThread.GetCallback().SignalIrcServerError(irc_error_code);
134 }
135 
136 void
irc_event_connect(irc_session_t * session,const char *,const char * origin,const char **,unsigned)137 irc_event_connect(irc_session_t *session, const char * /*irc_event*/, const char *origin, const char ** /*params*/, unsigned /*count*/)
138 {
139 	IrcContext *context = static_cast<IrcContext *>(irc_get_ctx(session));
140 
141 	context->ircThread.GetCallback().SignalIrcConnect(origin);
142 	irc_cmd_join(session, context->channel.c_str(), context->channelPassword.c_str());
143 }
144 
145 void
irc_event_join(irc_session_t * session,const char *,const char * origin,const char **,unsigned)146 irc_event_join(irc_session_t *session, const char * /*irc_event*/, const char *origin, const char ** /*params*/, unsigned /*count*/)
147 {
148 	// someone joined the channel.
149 	IrcContext *context = static_cast<IrcContext *>(irc_get_ctx(session));
150 
151 	if (context->nick == origin)
152 		context->ircThread.GetCallback().SignalIrcSelfJoined(context->nick, context->channel);
153 	else
154 		context->ircThread.GetCallback().SignalIrcPlayerJoined(origin);
155 }
156 
157 void
irc_event_nick(irc_session_t * session,const char *,const char * origin,const char ** params,unsigned count)158 irc_event_nick(irc_session_t *session, const char * /*irc_event*/, const char *origin, const char **params, unsigned count)
159 {
160 	// someone changed his/her nick
161 	IrcContext *context = static_cast<IrcContext *>(irc_get_ctx(session));
162 
163 	if (count && context->nick != params[0]) { // only act if this was not an auto-rename
164 		if (context->nick == origin)
165 			context->nick = params[0];
166 		context->ircThread.GetCallback().SignalIrcPlayerChanged(origin, params[0]);
167 	}
168 }
169 
170 void
irc_event_kick(irc_session_t * session,const char *,const char * origin,const char ** params,unsigned count)171 irc_event_kick(irc_session_t *session, const char * /*irc_event*/, const char *origin, const char **params, unsigned count)
172 {
173 	// someone got kicked
174 	IrcContext *context = static_cast<IrcContext *>(irc_get_ctx(session));
175 
176 	string byWhom(origin);
177 	string who;
178 	string reason;
179 	if (count >= 2)
180 		who = params[1];
181 	if (count >= 3)
182 		reason = params[2];
183 	context->ircThread.GetCallback().SignalIrcPlayerKicked(who, byWhom, reason);
184 	if (!who.empty())
185 		context->ircThread.GetCallback().SignalIrcPlayerLeft(who);
186 }
187 
188 void
irc_event_leave(irc_session_t * session,const char *,const char * origin,const char **,unsigned)189 irc_event_leave(irc_session_t *session, const char * /*irc_event*/, const char *origin, const char ** /*params*/, unsigned /*count*/)
190 {
191 	// someone left the channel.
192 	IrcContext *context = static_cast<IrcContext *>(irc_get_ctx(session));
193 
194 	context->ircThread.GetCallback().SignalIrcPlayerLeft(origin);
195 }
196 
197 void
irc_event_channel(irc_session_t * session,const char *,const char * origin,const char ** params,unsigned count)198 irc_event_channel(irc_session_t *session, const char * /*irc_event*/, const char *origin, const char **params, unsigned count)
199 {
200 	IrcContext *context = static_cast<IrcContext *>(irc_get_ctx(session));
201 
202 	if (count >= 2 && boost::algorithm::iequals(context->channel, params[0])) { // check whether this message is for our channel
203 		// Signal the message (if any) to GUI.
204 		if (*params[1] != 0)
205 			context->ircThread.GetCallback().SignalIrcChatMsg(origin, params[1]);
206 	}
207 }
208 
209 void
irc_event_unknown(irc_session_t * session,const char * irc_event,const char *,const char **,unsigned)210 irc_event_unknown(irc_session_t *session, const char * irc_event, const char * /*origin*/, const char ** /*params*/, unsigned /*count*/)
211 {
212 	IrcContext *context = static_cast<IrcContext *>(irc_get_ctx(session));
213 
214 	if (boost::algorithm::iequals(irc_event, "PONG")) {
215 		context->sendingBlocked = false;
216 		context->ircThread.FlushQueue();
217 	}
218 }
219 
220 void
irc_event_numeric(irc_session_t * session,unsigned irc_event,const char *,const char ** params,unsigned count)221 irc_event_numeric(irc_session_t * session, unsigned irc_event, const char * /*origin*/, const char **params, unsigned count)
222 {
223 	switch (irc_event) {
224 	case LIBIRC_RFC_ERR_NICKNAMEINUSE :
225 	case LIBIRC_RFC_ERR_NICKCOLLISION :
226 		irc_auto_rename_nick(session);
227 		break;
228 	case LIBIRC_RFC_RPL_TOPIC :
229 		break;
230 	case LIBIRC_RFC_RPL_NAMREPLY :
231 		if (count >= 4)
232 			irc_notify_player_list(session, params[3]);
233 		break;
234 	case LIBIRC_RFC_ERR_NOSUCHNICK :
235 	case LIBIRC_RFC_ERR_NOSUCHCHANNEL :
236 	case LIBIRC_RFC_ERR_CANNOTSENDTOCHAN :
237 	case LIBIRC_RFC_ERR_TOOMANYCHANNELS :
238 	case LIBIRC_RFC_ERR_WASNOSUCHNICK :
239 	case LIBIRC_RFC_ERR_TOOMANYTARGETS :
240 	case LIBIRC_RFC_ERR_NOSUCHSERVICE :
241 	case LIBIRC_RFC_ERR_NOORIGIN :
242 	case LIBIRC_RFC_ERR_NORECIPIENT :
243 	case LIBIRC_RFC_ERR_NOTEXTTOSEND :
244 	case LIBIRC_RFC_ERR_NOTOPLEVEL :
245 	case LIBIRC_RFC_ERR_WILDTOPLEVEL :
246 	case LIBIRC_RFC_ERR_BADMASK :
247 	case LIBIRC_RFC_ERR_UNKNOWNCOMMAND :
248 	case LIBIRC_RFC_ERR_NOMOTD :
249 	case LIBIRC_RFC_ERR_NOADMININFO :
250 	case LIBIRC_RFC_ERR_FILEERROR :
251 	case LIBIRC_RFC_ERR_NONICKNAMEGIVEN :
252 	case LIBIRC_RFC_ERR_ERRONEUSNICKNAME :
253 	case LIBIRC_RFC_ERR_UNAVAILRESOURCE :
254 	case LIBIRC_RFC_ERR_USERNOTINCHANNEL :
255 	case LIBIRC_RFC_ERR_NOTONCHANNEL :
256 	case LIBIRC_RFC_ERR_USERONCHANNEL :
257 	case LIBIRC_RFC_ERR_NOLOGIN :
258 	case LIBIRC_RFC_ERR_SUMMONDISABLED :
259 	case LIBIRC_RFC_ERR_USERSDISABLED :
260 	case LIBIRC_RFC_ERR_NOTREGISTERED :
261 	case LIBIRC_RFC_ERR_NEEDMOREPARAMS :
262 	case LIBIRC_RFC_ERR_ALREADYREGISTRED :
263 	case LIBIRC_RFC_ERR_NOPERMFORHOST :
264 	case LIBIRC_RFC_ERR_PASSWDMISMATCH :
265 	case LIBIRC_RFC_ERR_YOUREBANNEDCREEP :
266 	case LIBIRC_RFC_ERR_YOUWILLBEBANNED :
267 	case LIBIRC_RFC_ERR_KEYSET :
268 	case LIBIRC_RFC_ERR_CHANNELISFULL :
269 	case LIBIRC_RFC_ERR_UNKNOWNMODE :
270 	case LIBIRC_RFC_ERR_INVITEONLYCHAN :
271 	case LIBIRC_RFC_ERR_BANNEDFROMCHAN :
272 	case LIBIRC_RFC_ERR_BADCHANNELKEY :
273 	case LIBIRC_RFC_ERR_BADCHANMASK :
274 	case LIBIRC_RFC_ERR_NOCHANMODES :
275 	case LIBIRC_RFC_ERR_BANLISTFULL :
276 	case LIBIRC_RFC_ERR_NOPRIVILEGES :
277 	case LIBIRC_RFC_ERR_CHANOPRIVSNEEDED :
278 	case LIBIRC_RFC_ERR_CANTKILLSERVER :
279 	case LIBIRC_RFC_ERR_RESTRICTED :
280 	case LIBIRC_RFC_ERR_UNIQOPPRIVSNEEDED :
281 	case LIBIRC_RFC_ERR_NOOPERHOST :
282 	case LIBIRC_RFC_ERR_UMODEUNKNOWNFLAG :
283 	case LIBIRC_RFC_ERR_USERSDONTMATCH :
284 		irc_handle_server_error(session, irc_event);
285 		break;
286 	}
287 }
288 
IrcThread(const IrcThread & other)289 IrcThread::IrcThread(const IrcThread &other)
290 	: Thread(),
291 	  m_terminationTimer(boost::posix_time::time_duration(0, 0, 0), boost::timers::portable::microsec_timer::manual_start),
292 	  m_lastConnectTimer(boost::posix_time::time_duration(0, 0, 0), boost::timers::portable::microsec_timer::manual_start)
293 {
294 	m_callback = other.m_callback;
295 	m_context.reset(new IrcContext(*this));
296 
297 	IrcContext &context = GetContext();
298 	const IrcContext &otherContext = other.GetContext();
299 
300 	context.serverAddress	= otherContext.serverAddress;
301 	context.serverPort		= otherContext.serverPort;
302 	context.useIPv6			= otherContext.useIPv6;
303 	context.origNick		= otherContext.origNick;
304 	context.nick			= otherContext.origNick; // do not use changed nick.
305 	context.channel			= otherContext.channel;
306 	context.channelPassword	= otherContext.channelPassword;
307 }
308 
IrcThread(IrcCallback * callback)309 IrcThread::IrcThread(IrcCallback *callback)
310 	: m_callback(callback),
311 	  m_terminationTimer(boost::posix_time::time_duration(0, 0, 0), boost::timers::portable::microsec_timer::manual_start),
312 	  m_lastConnectTimer(boost::posix_time::time_duration(0, 0, 0), boost::timers::portable::microsec_timer::manual_start)
313 {
314 	assert(callback);
315 	m_context.reset(new IrcContext(*this));
316 }
317 
~IrcThread()318 IrcThread::~IrcThread()
319 {
320 	IrcContext &context = GetContext();
321 	if (context.session)
322 		irc_destroy_session(context.session);
323 }
324 
325 void
Init(const std::string & serverAddress,unsigned serverPort,bool ipv6,const std::string & nick,const std::string & channel,const std::string & channelPassword)326 IrcThread::Init(const std::string &serverAddress, unsigned serverPort, bool ipv6, const std::string &nick, const std::string &channel, const std::string &channelPassword)
327 {
328 	if (IsRunning() || serverAddress.empty() || nick.empty() || channel.empty())
329 		return; // TODO: throw exception
330 
331 	IrcContext &context = GetContext();
332 
333 	context.serverAddress	= serverAddress;
334 	context.serverPort		= serverPort;
335 	context.useIPv6			= ipv6;
336 	context.origNick		= nick;
337 	context.nick			= nick;
338 	context.channel			= channel;
339 	context.channelPassword	= channelPassword;
340 }
341 
342 void
SendChatMessage(const std::string & msg)343 IrcThread::SendChatMessage(const std::string &msg)
344 {
345 	IrcContext &context = GetContext();
346 	if (!context.sendingBlocked) {
347 		irc_cmd_msg(context.session, context.channel.c_str(), msg.c_str());
348 		context.sendCounter += msg.size();
349 
350 		if (context.sendCounter >= IRC_SEND_LIMIT_BYTES) {
351 			context.sendingBlocked = true;
352 			context.sendCounter = 0;
353 			SendPing();
354 		}
355 	} else
356 		context.sendQueue.push(msg);
357 }
358 
359 void
SendPing()360 IrcThread::SendPing()
361 {
362 	IrcContext &context = GetContext();
363 	irc_send_raw(context.session, "PING %s", context.serverAddress.c_str());
364 }
365 
366 void
SignalTermination()367 IrcThread::SignalTermination()
368 {
369 	Thread::SignalTermination();
370 	irc_cmd_quit(GetContext().session, NULL);
371 }
372 
373 void
Main()374 IrcThread::Main()
375 {
376 	do {
377 		if (IrcInit())
378 			IrcMain(); // Will loop until terminated.
379 	} while (!ShouldTerminate() && GetContext().session && !irc_is_connected(GetContext().session) && m_lastConnectTimer.elapsed().total_seconds() > IRC_MIN_RECONNECT_INTERVAL_SEC);
380 }
381 
382 bool
IrcInit()383 IrcThread::IrcInit()
384 {
385 	bool retVal = false;
386 
387 	IrcContext &context = GetContext();
388 	if (context.session) {
389 		irc_destroy_session(context.session);
390 		context.session = NULL;
391 	}
392 	// Initialize libirc stuff.
393 	irc_callbacks_t callbacks;
394 	memset (&callbacks, 0, sizeof(callbacks));
395 
396 	callbacks.event_connect = irc_event_connect;
397 	callbacks.event_join = irc_event_join;
398 	callbacks.event_nick = irc_event_nick;
399 	callbacks.event_quit = irc_event_leave;
400 	callbacks.event_part = irc_event_leave;
401 	//callbacks.event_mode
402 	//callbacks.event_topic
403 	callbacks.event_kick = irc_event_kick;
404 	callbacks.event_channel = irc_event_channel;
405 	//callbacks.event_privmsg
406 	//callbacks.event_notice
407 	//callbacks.event_invite
408 	//callbacks.event_umode
409 	//callbacks.event_ctcp_rep
410 	//callbacks.event_ctcp_action
411 	callbacks.event_unknown = irc_event_unknown;
412 	callbacks.event_numeric = irc_event_numeric;
413 
414 	//callbacks.event_dcc_chat_req
415 	//callbacks.event_dcc_send_req
416 
417 	context.session = irc_create_session(&callbacks);
418 
419 	if (context.session) {
420 		irc_set_ctx(context.session, &context);
421 		// We want nicknames only, strip them from nick!host.
422 		irc_option_set(context.session, LIBIRC_OPTION_STRIPNICKS);
423 		retVal = true;
424 	}
425 	return retVal;
426 }
427 
428 void
IrcMain()429 IrcThread::IrcMain()
430 {
431 	m_lastConnectTimer.restart();
432 	IrcContext &context = GetContext();
433 
434 	irc_session_t *s = context.session;
435 
436 	bool connected = false;
437 	if (context.useIPv6)
438 		connected = irc_connect6(s, context.serverAddress.c_str(), context.serverPort, 0, context.nick.c_str(), 0, 0) == 0;
439 	else
440 		connected = irc_connect(s, context.serverAddress.c_str(), context.serverPort, 0, context.nick.c_str(), 0, 0) == 0;
441 
442 	if (!connected)
443 		HandleIrcError(irc_errno(s));
444 	else {
445 		// Main loop.
446 		while (irc_is_connected(s)) {
447 			// Handle thread termination - gracefully.
448 			if (!m_terminationTimer.is_running()) {
449 				if (ShouldTerminate())
450 					m_terminationTimer.start();
451 			} else {
452 				if (m_terminationTimer.elapsed().total_milliseconds() > IRC_WAIT_TERMINATION_MSEC)
453 					break;
454 			}
455 
456 			struct timeval timeout;
457 			fd_set readSet, writeSet;
458 			int maxfd = 0;
459 
460 
461 			FD_ZERO(&readSet);
462 			FD_ZERO(&writeSet);
463 			timeout.tv_sec = 0;
464 			timeout.tv_usec = IRC_RECV_TIMEOUT_MSEC * 1000;
465 
466 			irc_add_select_descriptors(s, &readSet, &writeSet, &maxfd);
467 
468 			int selectResult = select(maxfd + 1, &readSet, &writeSet, 0, &timeout);
469 			if (selectResult == -1) {
470 				GetCallback().SignalIrcError(ERR_IRC_SELECT_FAILED);
471 				break;
472 			}
473 
474 			if (irc_process_select_descriptors(s, &readSet, &writeSet) != 0) {
475 				int errorCode = irc_errno(s);
476 				if (errorCode) {
477 					HandleIrcError(errorCode);
478 					break;
479 				}
480 			}
481 			Msleep(10); // paranoia
482 		}
483 	}
484 }
485 
486 void
FlushQueue()487 IrcThread::FlushQueue()
488 {
489 	IrcContext &context = GetContext();
490 
491 	while (!context.sendingBlocked && !context.sendQueue.empty()) {
492 		string msg(context.sendQueue.front());
493 		context.sendQueue.pop();
494 		SendChatMessage(msg);
495 	}
496 }
497 
498 void
HandleIrcError(int errorCode)499 IrcThread::HandleIrcError(int errorCode)
500 {
501 	int internalErrorCode = ERR_IRC_INTERNAL;
502 	switch(errorCode) {
503 	case LIBIRC_ERR_RESOLV:
504 	case LIBIRC_ERR_SOCKET:
505 	case LIBIRC_ERR_CONNECT:
506 		internalErrorCode = ERR_IRC_CONNECT_FAILED;
507 		break;
508 	case LIBIRC_ERR_INVAL:
509 	case LIBIRC_ERR_STATE:
510 		internalErrorCode = ERR_IRC_INVALID_PARAM;
511 		break;
512 	case LIBIRC_ERR_CLOSED:
513 	case LIBIRC_ERR_TERMINATED:
514 		internalErrorCode = ERR_IRC_TERMINATED;
515 		break;
516 	case LIBIRC_ERR_READ:
517 		internalErrorCode = ERR_IRC_RECV_FAILED;
518 		break;
519 	case LIBIRC_ERR_WRITE:
520 		internalErrorCode = ERR_IRC_SEND_FAILED;
521 		break;
522 	case LIBIRC_ERR_TIMEOUT:
523 		internalErrorCode = ERR_IRC_TIMEOUT;
524 		break;
525 	}
526 	GetCallback().SignalIrcError(internalErrorCode);
527 }
528 
529 const IrcContext &
GetContext() const530 IrcThread::GetContext() const
531 {
532 	assert(m_context.get());
533 	return *m_context;
534 }
535 
536 IrcContext &
GetContext()537 IrcThread::GetContext()
538 {
539 	assert(m_context.get());
540 	return *m_context;
541 }
542 
543 IrcCallback &
GetCallback()544 IrcThread::GetCallback()
545 {
546 	assert(m_callback);
547 	return *m_callback;
548 }
549 
550