Erebot  latest
A modular IRC bot for PHP 7.0+
IrcConnection.php
1 <?php
2 /*
3  This file is part of Erebot, a modular IRC bot written in PHP.
4 
5  Copyright © 2010 François Poirotte
6 
7  Erebot is free software: you can redistribute it and/or modify
8  it under the terms of the GNU General Public License as published by
9  the Free Software Foundation, either version 3 of the License, or
10  (at your option) any later version.
11 
12  Erebot is distributed in the hope that it will be useful,
13  but WITHOUT ANY WARRANTY; without even the implied warranty of
14  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15  GNU General Public License for more details.
16 
17  You should have received a copy of the GNU General Public License
18  along with Erebot. If not, see <http://www.gnu.org/licenses/>.
19 */
20 
21 namespace Erebot;
22 
27 class IrcConnection implements \Erebot\Interfaces\IrcConnection
28 {
33  protected $config;
34 
36  protected $bot;
37 
39  protected $socket;
40 
42  protected $channelModules;
43 
45  protected $plainModules;
46 
48  protected $numerics;
49 
51  protected $events;
52 
54  protected $connected;
55 
57  protected $uriFactory;
58 
60  protected $numericProfile;
61 
63  protected $collator;
64 
66  protected $eventsProducer;
67 
69  protected $io;
70 
90  public function __construct(
91  \Erebot\Interfaces\Core $bot,
92  \Erebot\Interfaces\Config\Server $config = null,
93  $events = array()
94  ) {
95  $this->config = $config;
96  $this->bot = $bot;
97  $this->channelModules = array();
98  $this->plainModules = array();
99  $this->numerics = array();
100  $this->events = array();
101  $this->connected = false;
102  $this->io = new \Erebot\LineIO(\Erebot\LineIO::EOL_WIN);
103  $this->collator = new \Erebot\IrcCollator\RFC1459();
104  $this->eventsProducer = new \Erebot\IrcParser($this);
106  $this->eventsProducer->setEventClasses($events);
107  $this->setURIFactory('\\Erebot\\URI');
108  $this->setNumericProfile(new \Erebot\NumericProfile\RFC2812());
109 
110  $this->addEventHandler(
111  new \Erebot\EventHandler(
112  array($this, 'handleCapabilities'),
113  new \Erebot\Event\Match\Type(
114  '\\Erebot\\Event\\ServerCapabilities'
115  )
116  )
117  );
118 
119  $this->addEventHandler(
120  new \Erebot\EventHandler(
121  array($this, 'handleConnect'),
122  new \Erebot\Event\Match\Type(
123  '\\Erebot\\Interface\\Event\\Connect'
124  )
125  )
126  );
127  }
128 
132  public function __destruct()
133  {
134  $this->socket = null;
135  unset(
136  $this->events,
137  $this->numerics,
138  $this->config,
139  $this->bot,
140  $this->channelModules,
141  $this->plainModules,
142  $this->uriFactory
143  );
144  }
145 
153  public function getURIFactory()
154  {
155  return $this->uriFactory;
156  }
157 
165  public function setURIFactory($factory)
166  {
167  $reflector = new \ReflectionClass($factory);
168  if (!$reflector->implementsInterface('\\Erebot\\URIInterface')) {
169  throw new \Erebot\InvalidValueException(
170  'The factory must implement \\Erebot\\URIInterface'
171  );
172  }
173  $this->uriFactory = $factory;
174  }
175 
176  public function getNumericProfile()
177  {
178  return $this->numericProfile;
179  }
180 
181  public function setNumericProfile(\Erebot\NumericProfile\Base $profile)
182  {
183  $this->numericProfile = $profile;
184  }
185 
192  public function reload(\Erebot\Interfaces\Config\Server $config)
193  {
194  $this->loadModules(
195  $config,
196  \Erebot\Module\Base::RELOAD_ALL
197  );
198  $this->config = $config;
199  }
200 
214  protected function loadModules(
215  \Erebot\Interfaces\Config\Server $config,
216  $flags
217  ) {
218  $logger = \Plop\Plop::getInstance();
219 
220  $channelModules = $this->channelModules;
221  $plainModules = $this->plainModules;
222 
223  $newNetCfg = $config->getNetworkCfg();
224  $newChannels = $newNetCfg->getChannels();
225 
226  $oldNetCfg = $this->config->getNetworkCfg();
227  $oldChannels = $oldNetCfg->getChannels();
228 
229  // Keep whatever can be kept from the old
230  // channels-related module configurations.
231  foreach ($oldChannels as $chan => $oldChanCfg) {
232  try {
233  $newChanCfg = $newNetCfg->getChannelCfg($chan);
234  $newModules = $newChanCfg->getModules(false);
235  foreach ($oldChanCfg->getModules(false) as $module) {
236  if (!in_array($module, $newModules)) {
237  unset($this->channelModules[$chan][$module]);
238  } elseif (isset($this->channelModules[$chan][$module])) {
239  $this->channelModules[$chan][$module] =
240  clone $this->channelModules[$chan][$module];
241  }
242  }
243  } catch (\Erebot\NotFoundException $e) {
244  unset($this->channelModules[$chan]);
245  }
246  }
247 
248  // Keep whatever can be kept from the old
249  // generic module configurations.
250  $newModules = $config->getModules(true);
251  foreach ($this->config->getModules(true) as $module) {
252  if (!in_array($module, $newModules)) {
253  unset($this->plainModules[$module]);
254  } elseif (isset($this->plainModules[$module])) {
255  $this->plainModules[$module] =
256  clone $this->plainModules[$module];
257  }
258  }
259 
260  // Configure new modules, both channel-related
261  // and generic ones.
262  foreach ($newChannels as $chanCfg) {
263  $modules = $chanCfg->getModules(false);
264  $chan = $chanCfg->getName();
265  foreach ($modules as $module) {
266  try {
267  $this->loadModule(
268  $module,
269  $chan,
270  $flags,
271  $this->plainModules,
272  $this->channelModules
273  );
274  } catch (\Erebot\StopException $e) {
275  throw $e;
276  } catch (\Exception $e) {
277  $logger->warning($e->getMessage());
278  }
279  }
280  }
281 
282  foreach ($newModules as $module) {
283  try {
284  $this->loadModule(
285  $module,
286  null,
287  $flags,
288  $this->plainModules,
289  $this->channelModules
290  );
291  } catch (\Erebot\StopException $e) {
292  throw $e;
293  } catch (\Exception $e) {
294  $logger->warning($e->getMessage());
295  }
296  }
297 
298  // Unload old module instances.
299  foreach ($channelModules as $modules) {
300  foreach ($modules as $module) {
301  $module->unloadModule();
302  }
303  }
304  foreach ($plainModules as $module) {
305  $module->unloadModule();
306  }
307  }
308 
309  public function isConnected()
310  {
311  return $this->connected;
312  }
313 
314  public function connect()
315  {
316  if ($this->connected) {
317  return false;
318  }
319 
320  $logger = \Plop\Plop::getInstance();
321  $uris = $this->config->getConnectionURI();
322  $serverUri = new \Erebot\URI($uris[count($uris) - 1]);
323  $this->socket = null;
324 
325  $logger->info(
326  $this->bot->gettext('Loading required modules for "%(uri)s"...'),
327  array('uri' => $serverUri)
328  );
329  $this->loadModules(
330  $this->config,
331  \Erebot\Module\Base::RELOAD_ALL |
332  \Erebot\Module\Base::RELOAD_INIT
333  );
334 
335  try {
336  $nbTunnels = count($uris);
337  $factory = $this->uriFactory;
338  for ($i = 0; $i < $nbTunnels; $i++) {
339  $uri = new $factory($uris[$i]);
340  $scheme = $uri->getScheme();
341  $upScheme = strtoupper($scheme);
342 
343  if ($i + 1 == $nbTunnels) {
344  $cls = '\\Erebot\\Proxy\\EndPoint\\'.$upScheme;
345  } else {
346  $cls = '\\Erebot\\Proxy\\'.$upScheme;
347  }
348 
349  if ($scheme == 'base' || !class_exists($cls)) {
350  throw new \Erebot\InvalidValueException('Invalid class');
351  }
352 
353  $port = $uri->getPort();
354  if ($port === null) {
355  $port = getservbyname($scheme, 'tcp');
356  }
357  if (!is_int($port) || $port <= 0 || $port > 65535) {
358  throw new \Erebot\InvalidValueException('Invalid port');
359  }
360 
361  if ($this->socket === null) {
362  $this->socket = @stream_socket_client(
363  'tcp://' . $uri->getHost() . ':' . $port,
364  $errno,
365  $errstr,
366  ini_get('default_socket_timeout'),
367  STREAM_CLIENT_CONNECT
368  );
369 
370  if ($this->socket === false) {
371  throw new \Erebot\Exception('Could not connect');
372  }
373  }
374 
375  // We're not the last link of the chain.
376  if ($i + 1 < $nbTunnels) {
377  $proxy = new $cls($this->socket);
378  if (!($proxy instanceof \Erebot\Proxy\Base)) {
379  throw new \Erebot\InvalidValueException('Invalid class');
380  }
381 
382  $next = new $factory($uris[$i + 1]);
383  $proxy->proxify($uri, $next);
384  $logger->debug(
385  "Successfully established connection ".
386  "through proxy '%(uri)s'",
387  array('uri' => $uri->toURI(false, false))
388  );
389  } else {
390  // That's the endpoint.
391  $endPoint = new $cls();
392  if (!($endPoint instanceof \Erebot\Interfaces\Proxy\EndPoint)) {
393  throw new \Erebot\InvalidValueException('Invalid class');
394  }
395 
396  $query = $uri->getQuery();
397  $params = array();
398  if ($query !== null) {
399  parse_str($query, $params);
400  }
401 
402  stream_context_set_option(
403  $this->socket,
404  'ssl',
405  'verify_peer',
406  isset($params['verify_peer'])
407  ? \Erebot\Config\Proxy::parseBoolHelper(
408  $params['verify_peer']
409  )
410  : true
411  );
412 
413  stream_context_set_option(
414  $this->socket,
415  'ssl',
416  'allow_self_signed',
417  isset($params['allow_self_signed'])
418  ? \Erebot\Config\Proxy::parseBoolHelper(
419  $params['allow_self_signed']
420  )
421  : true
422  );
423 
424  stream_context_set_option(
425  $this->socket,
426  'ssl',
427  'ciphers',
428  isset($params['ciphers'])
429  ? $params['ciphers']
430  : 'HIGH'
431  );
432 
433  // Avoid unnecessary buffers
434  // and activate TLS encryption if required.
435  stream_set_write_buffer($this->socket, 0);
436  if ($endPoint->requiresSSL()) {
437  stream_socket_enable_crypto(
438  $this->socket,
439  true,
440  STREAM_CRYPTO_METHOD_TLS_CLIENT
441  );
442  }
443  }
444  }
445  } catch (\Exception $e) {
446  if ($this->socket) {
447  fclose($this->socket);
448  }
449 
450  throw new \Erebot\ConnectionFailureException(
451  sprintf(
452  "Unable to connect to '%s' (%s)",
453  $uris[count($uris) - 1],
454  $e->getMessage()
455  )
456  );
457  }
458 
459  $this->io->setSocket($this->socket);
460  $this->dispatch($this->eventsProducer->makeEvent('!Logon'));
461  return true;
462  }
463 
464  public function disconnect($quitMessage = null)
465  {
466  $logger = \Plop\Plop::getInstance();
467  $uris = $this->config->getConnectionURI();
468  $logger->info(
469  "Disconnecting from '%(uri)s' ...",
470  array('uri' => $uris[count($uris) - 1])
471  );
472 
473  // Purge send queue and send QUIT message to notify server.
474  $this->io->setSocket($this->socket);
475  $quitMessage =
476  \Erebot\Utils::stringifiable($quitMessage)
477  ? ' :'.$quitMessage
478  : '';
479  $this->io->push('QUIT'.$quitMessage);
480 
481  // Send any pending data in the outgoing buffer.
482  while ($this->io->inWriteQueue()) {
483  $this->io->write();
484  usleep(50000); // Sleep for 50ms.
485  }
486 
487  // Then kill the connection for real.
488  $this->bot->removeConnection($this);
489  if (is_resource($this->socket)) {
490  fclose($this->socket);
491  }
492 
493  $this->io->setSocket(null);
494  $this->socket = null;
495  $this->connected = false;
496  }
497 
498  public function getConfig($chan)
499  {
500  if ($chan === null) {
501  return $this->config;
502  }
503 
504  try {
505  $netCfg = $this->config->getNetworkCfg();
506  $chanCfg = $netCfg->getChannelCfg($chan);
507  unset($netCfg);
508  return $chanCfg;
509  } catch (\Erebot\NotFoundException $e) {
510  return $this->config;
511  }
512  }
513 
514  public function getSocket()
515  {
516  return $this->socket;
517  }
518 
519  public function getBot()
520  {
521  return $this->bot;
522  }
523 
524  public function getIO()
525  {
526  return $this->io;
527  }
528 
529  public function read()
530  {
531  $res = $this->io->read();
532  if ($res === false) {
533  $event = $this->eventsProducer->makeEvent('!Disconnect');
534  $this->dispatch($event);
535 
536  if (!$event->preventDefault()) {
537  $logger = \Plop\Plop::getInstance();
538  $logger->error('Disconnected');
539  throw new \Erebot\ConnectionFailureException('Disconnected');
540  }
541  }
542  return $res;
543  }
544 
546  public function process()
547  {
548  for ($i = $this->io->inReadQueue(); $i > 0; $i--) {
549  $this->eventsProducer->parseLine($this->io->pop());
550  }
551  }
552 
553  public function write()
554  {
555  if (!$this->io->inWriteQueue()) {
556  throw new \Erebot\NotFoundException(
557  'No outgoing data needs to be handled'
558  );
559  }
560 
561  $logger = \Plop\Plop::getInstance();
562 
563  try {
565  // or having the module's name hard-coded like that.
566  $rateLimiter = $this->getModule('\\Erebot\\Module\\RateLimiter', null, false);
567 
568  try {
569  // Ask politely if we can send our message.
570  if (!$rateLimiter->canSend()) {
571  return false;
572  }
573  } catch (\Exception $e) {
574  $logger->exception(
575  $this->bot->gettext(
576  'Got an exception from the rate-limiter module. '.
577  'Assuming implicit approval to send the message.'
578  ),
579  $e
580  );
581  }
582  } catch (\Erebot\NotFoundException $e) {
583  // No rate-limit in effect, send away!
584  }
585 
586  return $this->io->write();
587  }
588 
621  protected function realLoadModule(
622  $module,
623  $chan,
624  $flags,
625  &$plainModules,
626  &$channelModules
627  ) {
628  if ($chan !== null) {
629  if (isset($channelModules[$chan][$module])) {
630  return $channelModules[$chan][$module];
631  }
632  } elseif (isset($plainModules[$module])) {
633  return $plainModules[$module];
634  }
635 
636  if (!class_exists($module, true)) {
637  throw new \Erebot\InvalidValueException("No such class '$module'");
638  }
639 
640  if (!is_subclass_of($module, '\\Erebot\\Module\\Base')) {
641  throw new \Erebot\InvalidValueException(
642  "Invalid module! Not a subclass of \\Erebot\\Module\\Base."
643  );
644  }
645 
646  $reflector = new \ReflectionClass($module);
647  $instance = new $module($chan);
648  if ($chan === null) {
649  $plainModules[$module] = $instance;
650  } else {
651  $channelModules[$chan][$module] = $instance;
652  }
653 
654  $instance->reloadModule($this, $flags);
655  $logger = \Plop\Plop::getInstance();
656  $logger->info(
657  $this->bot->gettext("Successfully loaded module '%(module)s' [%(source)s]"),
658  array(
659  'module' => $module,
660  'source' => (substr($reflector->getFileName(), 0, 7) == 'phar://')
661  ? $this->bot->gettext('PHP archive')
662  : $this->bot->gettext('regular file'),
663  )
664  );
665  return $instance;
666  }
667 
668  public function loadModule($module, $chan = null)
669  {
670  return $this->realLoadModule(
671  $module,
672  $chan,
673  \Erebot\Module\Base::RELOAD_ALL,
674  $this->plainModules,
675  $this->channelModules
676  );
677  }
678 
679  public function getModules($chan = null)
680  {
681  if ($chan !== null) {
682  $chanModules = isset($this->channelModules[$chan])
683  ? $this->channelModules[$chan]
684  : array();
685  return $chanModules + $this->plainModules;
686  }
687  return $this->plainModules;
688  }
689 
690  public function getModule($name, $chan = null, $autoload = true)
691  {
692  if ($chan !== null) {
693  if (isset($this->channelModules[$chan][$name])) {
694  return $this->channelModules[$chan][$name];
695  }
696 
697  $netCfg = $this->config->getNetworkCfg();
698  $chanCfg = $netCfg->getChannelCfg($chan);
699  $modules = $chanCfg->getModules(false);
700  if (in_array($name, $modules, true)) {
701  if (!$autoload) {
702  throw new \Erebot\NotFoundException('No instance found');
703  }
704  return $this->loadModule($name, $chan);
705  }
706  }
707 
708  if (isset($this->plainModules[$name])) {
709  return $this->plainModules[$name];
710  }
711 
712  $modules = $this->config->getModules(true);
713  if (!in_array($name, $modules, true) || !$autoload) {
714  throw new \Erebot\NotFoundException('No instance found');
715  }
716 
717  return $this->loadModule($name, null);
718  }
719 
720  public function addNumericHandler(\Erebot\Interfaces\NumericHandler $handler)
721  {
722  $this->numerics[] = $handler;
723  }
724 
725  public function removeNumericHandler(\Erebot\Interfaces\NumericHandler $handler)
726  {
727  $key = array_search($handler, $this->numerics);
728  if ($key === false) {
729  throw new \Erebot\NotFoundException('No such numeric handler');
730  }
731  unset($this->numerics[$key]);
732  }
733 
734  public function addEventHandler(\Erebot\Interfaces\EventHandler $handler)
735  {
736  $this->events[] = $handler;
737  }
738 
739  public function removeEventHandler(\Erebot\Interfaces\EventHandler $handler)
740  {
741  $key = array_search($handler, $this->events);
742  if ($key === false) {
743  throw new \Erebot\NotFoundException('No such event handler');
744  }
745  unset($this->events[$key]);
746  }
747 
755  protected function dispatchEvent(\Erebot\Interfaces\Event\Base\Generic $event)
756  {
757  $logger = \Plop\Plop::getInstance();
758  $logger->debug(
759  $this->bot->gettext('Dispatching "%(type)s" event.'),
760  array('type' => get_class($event))
761  );
762  try {
763  foreach ($this->events as $handler) {
764  if ($handler->handleEvent($event) === false) {
765  break;
766  }
767  }
768  } catch (\Erebot\ErrorReportingException $e) {
769  // This should help make the code a little more "bug-free" (TM).
770  $logger->exception($this->bot->gettext('Code is not clean!'), $e);
771  $this->disconnect($e->getMessage());
772  }
773  }
774 
782  protected function dispatchNumeric(\Erebot\Interfaces\Event\Numeric $numeric)
783  {
784  $logger = \Plop\Plop::getInstance();
785  $logger->debug(
786  $this->bot->gettext('Dispatching numeric %(code)s.'),
787  array('code' => sprintf('%03d', $numeric->getCode()))
788  );
789  try {
790  foreach ($this->numerics as $handler) {
791  if ($handler->handleNumeric($numeric) === false) {
792  break;
793  }
794  }
795  } catch (\Erebot\ErrorReportingException $e) {
796  // This should help make the code a little more "bug-free" (TM).
797  $logger->exception($this->bot->gettext('Code is not clean!'), $e);
798  $this->disconnect($e->getMessage());
799  }
800  }
801 
802  public function dispatch(\Erebot\Interfaces\Event\Base\Generic $event)
803  {
804  if ($event instanceof \Erebot\Interfaces\Event\Numeric) {
805  return $this->dispatchNumeric($event);
806  }
807  return $this->dispatchEvent($event);
808  }
809 
810  public function isChannel($chan)
811  {
812  try {
813  $capabilities = $this->getModule('\\Erebot\\Module\\ServerCapabilities', null, false);
814  return $capabilities->isChannel($chan);
815  } catch (\Erebot\NotFoundException $e) {
816  // Ignore silently.
817  }
818 
819  if (!\Erebot\Utils::stringifiable($chan)) {
820  throw new \Erebot\InvalidValueException(
821  $this->bot->gettext('Bad channel name')
822  );
823  }
824 
825  $chan = (string) $chan;
826  if (!strlen($chan)) {
827  return false;
828  }
829 
830  // Restricted characters in channel names,
831  // as per RFC 2811 - (2.1) Namespace.
832  foreach (array(' ', ',', "\x07", ':') as $token) {
833  if (strpos($token, $chan) !== false) {
834  return false;
835  }
836  }
837 
838  if (strlen($chan) > 50) {
839  return false;
840  }
841 
842  // As per RFC 2811 - (2.1) Namespace.
843  return (strpos('#&+!', $chan[0]) !== false);
844  }
845 
856  public function handleCapabilities(
857  \Erebot\Interfaces\EventHandler $handler,
858  \Erebot\Event\ServerCapabilities $event
859  ) {
860  $module = $event->getModule();
861  $validMappings = array(
862  // This is already the default value, but we still define it
863  // in case setCollator() was called to change the default.
864  'rfc1459' => '\\Erebot\\IrcCollator\\RFC1459',
865  'strict-rfc1459' => '\\Erebot\\IrcCollator\\StrictRFC1459',
866  'ascii' => '\\Erebot\\IrcCollator\\ASCII',
867  );
868  $caseMapping = strtolower($module->getCaseMapping());
869  if (in_array($caseMapping, array_keys($validMappings))) {
870  $cls = $validMappings[$caseMapping];
871  $this->collator = new $cls();
872  }
873  }
874 
885  public function handleConnect(
886  \Erebot\Interfaces\EventHandler $handler,
887  \Erebot\Interfaces\Event\Connect $event
888  ) {
889  $this->connected = true;
890  }
891 
898  public function setCollator(\Erebot\Interfaces\IrcCollator $collator)
899  {
900  $this->collator = $collator;
901  }
902 
910  public function getCollator()
911  {
912  return $this->collator;
913  }
914 
915  public function getEventsProducer()
916  {
917  return $this->eventsProducer;
918  }
919 }
$numerics
A list of numeric handlers.
setCollator(\Erebot\Interfaces\IrcCollator $collator)
Provides core functionalities for Erebot.
Definition: Core.php:59
$connected
Whether this connection is actually... well, connected.
Handles a (possibly encrypted) connection to an IRC server.
$socket
The underlying socket, represented as a stream.
handleCapabilities(\Erebot\Interfaces\EventHandler $handler,\Erebot\Event\ServerCapabilities $event)
reload(\Erebot\Interfaces\Config\Server $config)
handleConnect(\Erebot\Interfaces\EventHandler $handler,\Erebot\Interfaces\Event\Connect $event)
$events
A list of event handlers.
$numericProfile
Numeric profile.
$plainModules
Maps modules names to modules instances.
__construct(\Erebot\Interfaces\Core $bot,\Erebot\Interfaces\Config\Server $config=null, $events=array())
loadModules(\Erebot\Interfaces\Config\Server $config, $flags)
$uriFactory
Factory to use to parse URI.
dispatchEvent(\Erebot\Interfaces\Event\Base\Generic $event)
process()
Processes commands queued in the input buffer.
$bot
A bot object implementing the Erebot::Interfaces::Core interface.
$collator
Collator for IRC nicknames.
dispatchNumeric(\Erebot\Interfaces\Event\Numeric $numeric)
An event handler which will call a callback function/method whenever a set of conditions are met...
$io
I/O manager for the socket.
realLoadModule($module, $chan, $flags, &$plainModules, &$channelModules)
$eventsProducer
Class to use to parse IRC messages and produce events from them.
$channelModules
Maps channels to their loaded modules.