diff multiclientrelay/multiclientrelay2.php @ 0:dd81c38b513a

Initial commit
author Ivo Smits <Ivo@UCIS.nl>
date Mon, 28 Feb 2011 00:49:07 +0100
parents
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/multiclientrelay/multiclientrelay2.php	Mon Feb 28 00:49:07 2011 +0100
@@ -0,0 +1,455 @@
+<?php
+/* Copyright 2010 Ivo Smits <Ivo@UCIS.nl>. All rights reserved.
+   Redistribution and use in source and binary forms, with or without modification, are
+   permitted provided that the following conditions are met:
+
+   1. Redistributions of source code must retain the above copyright notice, this list of
+      conditions and the following disclaimer.
+
+   2. Redistributions in binary form must reproduce the above copyright notice, this list
+      of conditions and the following disclaimer in the documentation and/or other materials
+      provided with the distribution.
+
+   THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED
+   WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
+   FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR
+   CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+   CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+   SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+   ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+   NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+   ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+   The views and conclusions contained in the software and documentation are those of the
+   authors and should not be interpreted as representing official policies, either expressed
+   or implied, of Ivo Smits.*/
+
+if (defined('APP_LOADED')) return;
+define('APP_LOADED', TRUE);
+if (!defined('CONFIGFILE')) define('CONFIGFILE', './config.php');
+
+print("UCIS UDPMSG3 Multi client IRC relay (c) 2010 Ivo Smits <Ivo@UCIS.nl>\n");
+print("More information: http://wiki.ucis.nl/UDPMSG3\n");
+print("\n");
+
+$irc_channels = array();
+$irc_users = array();
+$irc_clients = array();
+
+$sock_fd_connecting = array();
+$sock_fd_connected = array();
+$sock_obj = array();
+
+class Channel {
+	public $uname = NULL;
+	public $iname = NULL;
+}
+class User {
+	public $iname = NULL;
+	public $channels = array();
+}
+class UDPMSGClient {
+	public $socket = NULL;
+	public $buffer = '';
+	public $channels = array();
+	public function select_read() {
+		if (!$this->udpmsg_read()) die("UDPMSG: Error: closing connection.\n");
+	}
+	public function select_error() { die("UDPMSG: Error: select error.\n"); }
+	private function udpmsg_read() {
+		$msg = socket_read($this->socket, 1024);
+		if (!strlen($msg)) {
+			fprintf(STDERR, "UDPMSG: End of file\n");
+			return FALSE;
+		}
+		$udpmsg_buffer = &$this->buffer;
+		$udpmsg_buffer .= $msg;
+		while (strlen($udpmsg_buffer) > 2) {
+			$len = ord($udpmsg_buffer[0]) * 256 + ord($udpmsg_buffer[1]);
+			if ($len <= 0 || $len > 1024) {
+				fprintf(STDERR, "UDPMSG: Error: protocol error\n");
+				return FALSE;
+			}
+			if (strlen($udpmsg_buffer) < 2 + $len) break;
+			$msg = substr($udpmsg_buffer, 2, $len);
+			$udpmsg_buffer = substr($udpmsg_buffer, 2 + $len);
+			$parts = explode("\0", $msg);
+			$ret = array();
+			for ($i = 0; $i < count($parts)-1; $i += 2) $ret[$parts[$i]] = $parts[$i+1];
+			$this->udpmsg_process($ret);
+		}
+		return TRUE;
+	}
+	function udpmsg_process($ret) {
+		global $config, $ircbot;
+		if (!isset($ret['CHN']) || !isset($ret['CMD']) || !isset($ret['USR'])) return;
+		if (!array_key_exists($ret['CHN'], $this->channels)) return;
+		$ch = $this->channels[$ret['CHN']];
+		$net = isset($ret['NET']) ? preg_replace('/[\x00\x10-\x13]/', '', $ret['NET']) : NULL;
+		$usr = preg_replace('/[\x00\x10-\x13]/', '', $ret['USR']);
+		switch ($ret['CMD']) {
+			case 'MSG':
+				if (!isset($ret['MSG'])) break;
+				$msg = preg_replace('/[\x00\x10-\x13]/', '', $ret['MSG']);
+				$client = $this->get_irc_client($usr, $ch);
+				if ($client === NULL) {
+					$pfx = '['.$usr.($net?' @ '.$net:'').'] ';
+					if (ord($msg[0]) == 1) {
+						if (substr($message, 1, 6) != 'ACTION') break;
+						$msg = chr(1).'ACTION '.$pfx.substr($msg, 8, -1).chr(1);
+					} else {
+						$msg = $pfx.$msg;
+					}
+					$client = $ircbot;
+				}
+				$client->irc_channel_send($ch, $msg);
+				break;
+			case 'JOIN':
+				if ($this->get_irc_client($usr, $ch) === NULL && $config['irc']['joinpartmessages']) $ircbot->irc_channel_send($ch, '* '.$usr.' has joined'.($net?' on network '.$net:''));
+				break;
+			case 'PART':
+				if (!$this->irc_client_part($usr, $ch) && $config['irc']['joinpartmessages']) $ircbot->irc_channel_send($ch, '* '.$usr.' has left'.($net?' on network '.$net:''));
+				break;
+			case 'NICK':
+				if (!isset($ret['NEWNICK'])) break;
+				$newnick = preg_replace('/[\x00\x10-\x13]/', '', $ret['NEWNICK']);
+				if ($config['irc']['joinpartmessages']) $ircbot->irc_channel_send($ch, '* '.$usr.' has changed their nickname to '.$newnick.($net?' on network '.$net:''));
+				$this->irc_client_part($usr, $ch);
+				$this->get_irc_client($usr, $ch);
+				break;
+		}
+	}
+	function send($arr) {
+		global $config;
+		$tmp = array();
+		$arr['DUMMY'] = rand(0, 999999);
+		$arr['NET'] = $config['udpmsg']['netname'];
+		foreach ($arr as $key => $value) { $tmp[] = $key; $tmp[] = $value; }
+		$tmp[] = '';
+		$msg = implode("\0", $tmp);
+		$len = strlen($msg);
+		if ($len > 1024) {
+			fprintf(STDERR, "UDPMSG: Error: message too long!\n");
+			return;
+		}
+		$lens = chr(floor($len / 256)).chr($len % 256);
+		socket_write($this->socket, $lens.$msg);
+	}
+	function irc_client_part($name, $ch) {
+		if (!isset($irc_clients[$name])) return FALSE;
+		$client = $irc_clients[$name];
+		unset($client->channels[$ch->iname]);
+		unset($client->joinchannels[$ch->iname]);
+		if (!count($client->channels) && !count($client->joinchannels)) {
+			$client->irc_send('QUIT :No remaining channels.');
+			$client->close();
+		} else {
+			$client->irc_send('PART :'.$ch->iname);
+		}
+	}
+	function get_irc_client($name, $ch) {
+		global $irc_clients, $config;
+		if (!isset($irc_clients[$name])) {
+			if (count($irc_clients) < $config['irc']['clientconnections']) {
+				$client = new Client();
+				$client->uname = $client->iname = $name;
+				$client->socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
+				socket_connect($client->socket, $config['irc']['host'], $config['irc']['port']);
+				$client->joinchannels[$ch->iname] = $ch;
+				$irc_clients[$name] = $client;
+			}
+			return NULL;
+		}
+		$client = $irc_clients[$name];
+		if (!$client->connected || !$client->identified) return NULL;
+		if (!isset($client->channels[$ch->iname])) {
+			$client->joinchannels[$ch->iname] = $ch;
+			$client->irc_send('JOIN :'.$ch->iname);
+			return NULL;
+		}
+		return $client;
+	}
+}	
+class IRCClientBase {
+	public $iname = NULL;
+	public $channels = array();
+	public $socket = NULL;
+	public $buffer = '';
+	public $connected = FALSE;
+	public $identified = FALSE;
+	public function irc_read() {
+		$newdata = socket_read($this->socket, 1024);
+		if ($newdata === FALSE || !strlen($newdata)) return FALSE;
+		$irc_buffer = $this->buffer . $newdata;
+		$offset = 0;
+		$len = strlen($irc_buffer);
+		while ($offset < $len) {
+			$posa = strpos($irc_buffer, "\n", $offset);
+			$posb = strpos($irc_buffer, "\r", $offset);
+			if ($posa !== FALSE && $posb !== FALSE) $pos = min($posa, $posb);
+			else if ($posa !== FALSE) $pos = $posa;
+			else if ($posb !== FALSE) $pos = $posb;
+			else break;
+			$line = substr($irc_buffer, $offset, $pos - $offset);
+			if (strlen($line)) {
+				print('IR: '.$line."\n");
+				$partsa = explode(' :', $line, 2);
+				$parts = explode(' ', $partsa[0]);
+				$sender = ($parts[0][0] == ':') ? substr(array_shift(&$parts), 1) : NULL;
+				$command = array_shift(&$parts);
+				if (count($partsa) > 1) array_push(&$parts, $partsa[1]);
+				$partsa = explode('!', $sender);
+				$sendernick = $partsa[0];
+				if (!$this->process($sender, $sendernick, $command, $parts)) return FALSE;
+			}
+			$offset = $pos + 1;
+		}
+		$this->buffer = ($offset == $len) ? '' : substr($irc_buffer, $offset);
+		return TRUE;
+	}
+	public function irc_send($line) {
+		print('IW: '.$line."\n");
+		$line .= "\r\n";
+		socket_send($this->socket, $line, strlen($line), 0);
+	}
+	public function irc_channel_send($ch, $msg) {
+		if (!$this->identified) return;
+		$this->irc_send('PRIVMSG '.$ch->iname.' :'.$msg);
+	}
+}
+class MasterClient extends IRCClientBase {
+	public function select_error() { die("IRC: Error: select error.\n"); }
+	public function select_read() {
+		if (!$this->irc_read()) die("IRC: read error.\n");
+	}
+	public function process($sender, $sendernick, $command, $parts) {
+		global $irc_channels, $config, $irc_users, $udpmsg;
+		switch (strtoupper($command)) {
+			case '001': //Welcome
+				$this->iname = $parts[0];
+				$this->identified = TRUE;
+				irc_user_add($this, TRUE);
+				foreach ($irc_channels as $ch) $this->irc_send("JOIN :".$ch->iname);
+				break;
+			case '433': //Nickname in use
+				$this->irc_send("NICK ".$config['irc']['nick'].rand(100,999));
+				break;
+			case 'PING':
+				$this->irc_send('PONG :'.$parts[0]);
+				break;
+			case 'NICK': //Nickname change
+				//if (strcasecmp($sendernick, $this->iname)) $this->iname = $parts[0];
+				$usr = $this->irc_getuser($sendernick, FALSE);
+				if ($usr === NULL) break;
+				$this->irc_userchannels($usr, array('CMD' => 'NICK', 'NEWNICK' => $parts[0]));
+				irc_user_delete($usr);
+				$usr->iname = $parts[0];
+				irc_user_add($usr);
+				break;
+			case 'JOIN':
+				if (!strcasecmp($sendernick, $this->iname)) break;
+				$chs = explode(',', $parts[0]);
+				$usr = NULL;
+				foreach ($chs as $chn) {
+					$ch = $this->irc_getchannel($chn);
+					if ($ch === NULL) continue;
+					if ($usr === NULL) $usr = $this->irc_getuser($sendernick, TRUE);
+					$usr->channels[$ch->iname] = $ch;
+					if (!is_a($usr, 'Client')) $udpmsg->send(array('CMD' => 'JOIN', 'CHN' => $ch->uname, 'USR' => $usr->iname));
+				}
+				break;
+			case '353': //:server 353 me = channel :nickname nickname nickname
+				$ch = $this->irc_getchannel($parts[2]);
+				if ($ch === NULL) break;
+				$partsa = explode(' ', $parts[3]);
+				foreach ($partsa as $un) {
+					if (!strlen($un)) continue;
+					$usr = $this->irc_getuser(ltrim($un, '~&@%+'), TRUE);
+					$usr->channels[$ch->iname] = $ch;
+					if (!is_a($usr, 'Client')) $udpmsg->send(array('CMD' => 'JOIN', 'CHN' => $ch->uname, 'USR' => $usr->iname));
+				}
+				break;
+			case 'PART':
+				$usr = $this->irc_getuser($sendernick, FALSE);
+				if ($usr === NULL) break;
+				$chs = explode(',', $parts[0]);
+				foreach ($chs as $chn) $this->irc_part($usr, $chn);
+				break;
+			case 'KICK':
+				$usr = $this->irc_getuser($parts[1], FALSE);
+				if (is_a($usr, 'Client')) break;
+				$this->irc_part($usr, $parts[0]);
+				break;
+			case 'QUIT':
+				$usr = $this->irc_getuser($sendernick, FALSE);
+				if ($usr === NULL) break;
+				if (is_a($usr, 'Client')) break;
+				$this->irc_userchannels($usr, array('CMD' => 'PART'));
+				irc_user_delete($usr, TRUE);
+				break;
+			case 'PRIVMSG':
+			case 'NOTICE':
+				$ch = $this->irc_getchannel($parts[0]);
+				if (is_a($this->irc_getuser($sendernick, FALSE), 'Client')) break;
+				if ($ch === NULL) break;
+				$udpmsg->send(array('CMD' => 'MSG', 'CHN' => $ch->uname, 'MSG' => $parts[1], 'USR' => $sendernick));
+				break;
+		}
+		return TRUE;
+	}
+	function irc_userchannels($usr, $msg) {
+		global $udpmsg;
+		$msg['USR'] = $usr->iname;
+		foreach ($usr->channels as $ch) {
+			$msg['CHN'] = $ch->uname;
+			$udpmsg->send($msg);
+		}
+	}
+	function irc_part($usr, $chn) {
+		global $udpmsg;
+		$ch = $this->irc_getchannel($chn);
+		if ($ch === NULL || $usr === NULL) return;
+		unset($usr->channels[$ch->name]);
+		if (!is_a($usr, 'Client')) $udpmsg->send(array('CMD' => 'PART', 'CHN' => $ch->uname, 'USR' => $usr->iname));
+		if (!count($usr->channels) && is_a($usr, 'User')) irc_user_delete($user, TRUE);
+	}
+	function irc_getchannel($chn) {
+		global $irc_channels;
+		$chn = strtoupper($chn);
+		if (!array_key_exists($chn, $irc_channels)) return NULL;
+		return $irc_channels[$chn];
+	}
+	function irc_getuser($nick, $create = TRUE) {
+		global $irc_users;
+		$snick = strtoupper($nick);
+		if (array_key_exists($snick, $irc_users)) return $irc_users[$snick];
+		if (!$create) return NULL;
+		$usr = new User();
+		$usr->iname = $nick;
+		irc_user_add($usr, TRUE);
+		return $usr;
+	}
+}
+class Client extends IRCClientBase {
+	public $uname = NULL;
+	public $joinchannels = array();
+	public function select_error() {
+		$this->close();
+	}
+	public function select_write() {
+		$this->connected = TRUE;
+		$this->irc_send('USER '.$this->iname.' host server :'.$this->iname);
+		$this->irc_send('NICK '.$this->iname);
+	}
+	public function select_read() {
+		if (!$this->irc_read()) {
+			print("IRCC: read error.\n");
+			$this->close();
+		}
+	}
+	public function process($sender, $sendernick, $command, $parts) {
+		switch (strtoupper($command)) {
+			case '001': //Welcome
+				$this->iname = $parts[0];
+				$this->identified = TRUE;
+				irc_user_add($this, TRUE);
+				foreach ($this->joinchannels as $ch) $this->irc_send("JOIN :".$ch->iname);
+				break;
+			case '433': //Nickname in use
+				$this->irc_send("NICK ".$this->iname.rand(100,999));
+				break;
+			case 'PING':
+				$this->irc_send('PONG :'.$parts[0]);
+				break;
+			//Other messages are also received and processed by the master relay bot, so we ignore them here
+		}
+		return TRUE;
+	}
+	public function close() {
+		global $irc_clients;
+		unset($irc_clients[$this->uname]);
+		if ($this->identified) irc_user_delete($this, TRUE);
+		socket_close($this->socket);
+		socket_delete($this);
+	}
+}
+
+srand();
+
+print("Loading configuration...\n");
+if (!isset($config)) require constant('CONFIGFILE');
+if (!isset($config['irc']['joinpartmessages'])) $config['irc']['joinpartmessages'] = TRUE;
+if (!isset($config['udpmsg']['send_alive'])) $config['udpmsg']['send_alive'] = 1800;
+
+$udpmsg = new UDPMSGClient();
+$udpmsg->socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
+socket_connect($udpmsg->socket, $config['udpmsg']['host'], $config['udpmsg']['port']);
+socket_add_connected($udpmsg);
+
+$ircbot = new MasterClient();
+$ircbot->socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
+socket_connect($ircbot->socket, $config['irc']['host'], $config['irc']['port']);
+socket_add_connected($ircbot);
+$ircbot->irc_send('USER '.$config['irc']['ident'].' host server :'.$config['irc']['realname']);
+$ircbot->irc_send('NICK '.$config['irc']['nick']);
+
+foreach ($config['channels'] as $iname => $uname) {
+	$ch = new Channel();
+	$ch->iname = $iname;
+	$ch->uname = $uname;
+	$irc_channels[strtoupper($ch->iname)] = $ch;
+	$udpmsg->channels[$ch->uname] = $ch;
+}
+
+print("Starting IRC Relay...\n");
+
+function socket_add_connecting($obj) {
+	global $sock_fd_connecting, $sock_obj;
+	$sock_fd_connecting[(int)$obj->socket] = $obj->socket;
+	$sock_obj[(int)$obj->socket] = $obj;
+}
+function socket_add_connected($obj) {
+	global $sock_fd_connected, $sock_obj;
+	$sock_fd_connected[(int)$obj->socket] = $obj->socket;
+	$sock_obj[(int)$obj->socket] = $obj;
+}
+function socket_delete($obj) {
+	global $sock_fd_connecting, $sock_fd_connected, $sock_obj;
+	unset($sock_fd_connecting[(int)$obj->socket]);
+	unset($sock_fd_connected[(int)$obj->socket]);
+	unset($sock_obj[(int)$obj->socket]);
+}
+
+function irc_user_add($user, $replace = FALSE) {
+	global $irc_users;
+	$name = strtoupper($user->iname);
+	if (!$replace && isset($irc_users[$name])) return FALSE;
+	$irc_users[$name] = $user;
+	return TRUE;
+}
+function irc_user_delete($user, $checkobject = FALSE) {
+	global $irc_users;
+	$name = strtoupper(is_object($user) ? $user->iname : $user);
+	if (!isset($irc_users[$name])) return TRUE;
+	if ($checkobject && $irc_users[$name] !== $user) return FALSE;
+	unset($irc_users[$name]);
+	return TRUE;
+}
+
+$atime = 0;
+print("Running\n");
+while (TRUE) {
+	$selread = $sock_fd_connected;
+	$selwrite = $sock_fd_connecting;
+	$selerror = array_merge($selread, $selwrite);
+	socket_select(&$selread, &$selwrite, &$selerror, 120);
+	foreach ($selread as $socket) if (isset($sock_obj[(int)$socket])) $sock_obj[(int)$socket]->select_read($socket);
+	foreach ($selwrite as $socket) if (isset($sock_obj[(int)$socket])) $sock_obj[(int)$socket]->select_write($socket);
+	foreach ($selerror as $socket) if (isset($sock_obj[(int)$socket])) $sock_obj[(int)$socket]->select_error($socket);
+
+	$ctime = time();
+	if ($atime < $ctime) {
+		foreach ($irc_users as $usr) if (is_a($usr, 'IUser')) $ircbot->irc_userchannels($usr, array('CMD' => 'ALIVE'));
+		$atime = $ctime + $config['udpmsg']['send_alive'];
+	}
+}