server.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501
  1. # -*- coding: utf-8 -*-
  2. """
  3. irc/server.py
  4. This server has basic support for:
  5. * Connecting
  6. * Channels
  7. * Nicknames
  8. * Public/private messages
  9. It is MISSING support for notably:
  10. * Server linking
  11. * Modes (user and channel)
  12. * Proper error reporting
  13. * Basically everything else
  14. It is mostly useful as a testing tool or perhaps for building something like a
  15. private proxy on. Do NOT use it in any kind of production code or anything that
  16. will ever be connected to by the public.
  17. """
  18. #
  19. # Very simple hacky ugly IRC server.
  20. #
  21. # Todo:
  22. # - Encode format for each message and reply with events.codes['needmoreparams']
  23. # - starting server when already started doesn't work properly. PID file is not changed, no error messsage is displayed.
  24. # - Delete channel if last user leaves.
  25. # - [ERROR] <socket.error instance at 0x7f9f203dfb90> (better error msg required)
  26. # - Empty channels are left behind
  27. # - No Op assigned when new channel is created.
  28. # - User can /join multiple times (doesn't add more to channel, does say 'joined')
  29. # - PING timeouts
  30. # - Allow all numerical commands.
  31. # - Users can send commands to channels they are not in (PART)
  32. # Not Todo (Won't be supported)
  33. # - Server linking.
  34. from __future__ import print_function, absolute_import
  35. import argparse
  36. import logging
  37. import socket
  38. import select
  39. import re
  40. import six
  41. from six.moves import socketserver
  42. import jaraco.logging
  43. import irc.client
  44. from . import events
  45. from . import buffer
  46. SRV_WELCOME = "Welcome to {__name__} v{irc.client.VERSION}.".format(**locals())
  47. log = logging.getLogger(__name__)
  48. class IRCError(Exception):
  49. """
  50. Exception thrown by IRC command handlers to notify client of a
  51. server/client error.
  52. """
  53. def __init__(self, code, value):
  54. self.code = code
  55. self.value = value
  56. def __str__(self):
  57. return repr(self.value)
  58. @classmethod
  59. def from_name(cls, name, value):
  60. return cls(events.codes[name], value)
  61. class IRCChannel(object):
  62. """
  63. An IRC channel.
  64. """
  65. def __init__(self, name, topic='No topic'):
  66. self.name = name
  67. self.topic_by = 'Unknown'
  68. self.topic = topic
  69. self.clients = set()
  70. class IRCClient(socketserver.BaseRequestHandler):
  71. """
  72. IRC client connect and command handling. Client connection is handled by
  73. the ``handle`` method which sets up a two-way communication with the client.
  74. It then handles commands sent by the client by dispatching them to the
  75. handle_ methods.
  76. """
  77. class Disconnect(BaseException): pass
  78. def __init__(self, request, client_address, server):
  79. self.user = None
  80. self.host = client_address # Client's hostname / ip.
  81. self.realname = None # Client's real name
  82. self.nick = None # Client's currently registered nickname
  83. self.send_queue = [] # Messages to send to client (strings)
  84. self.channels = {} # Channels the client is in
  85. # On Python 2, use old, clunky syntax to call parent init
  86. if six.PY2:
  87. socketserver.BaseRequestHandler.__init__(self, request,
  88. client_address, server)
  89. return
  90. super().__init__(request, client_address, server)
  91. def handle(self):
  92. log.info('Client connected: %s', self.client_ident())
  93. self.buffer = buffer.LineBuffer()
  94. try:
  95. while True:
  96. self._handle_one()
  97. except self.Disconnect:
  98. self.request.close()
  99. def _handle_one(self):
  100. """
  101. Handle one read/write cycle.
  102. """
  103. ready_to_read, ready_to_write, in_error = select.select(
  104. [self.request], [self.request], [self.request], 0.1)
  105. if in_error:
  106. raise self.Disconnect()
  107. # Write any commands to the client
  108. while self.send_queue and ready_to_write:
  109. msg = self.send_queue.pop(0)
  110. self._send(msg)
  111. # See if the client has any commands for us.
  112. if ready_to_read:
  113. self._handle_incoming()
  114. def _handle_incoming(self):
  115. try:
  116. data = self.request.recv(1024)
  117. except Exception:
  118. raise self.Disconnect()
  119. if not data:
  120. raise self.Disconnect()
  121. self.buffer.feed(data)
  122. for line in self.buffer:
  123. line = line.decode('utf-8')
  124. self._handle_line(line)
  125. def _handle_line(self, line):
  126. try:
  127. log.debug('from %s: %s' % (self.client_ident(), line))
  128. command, sep, params = line.partition(' ')
  129. handler = getattr(self, 'handle_%s' % command.lower(), None)
  130. if not handler:
  131. _tmpl = 'No handler for command: %s. Full line: %s'
  132. log.info(_tmpl % (command, line))
  133. raise IRCError.from_name('unknowncommand',
  134. '%s :Unknown command' % command)
  135. response = handler(params)
  136. except AttributeError as e:
  137. log.error(six.text_type(e))
  138. raise
  139. except IRCError as e:
  140. response = ':%s %s %s' % (self.server.servername, e.code, e.value)
  141. log.error(response)
  142. except Exception as e:
  143. response = ':%s ERROR %r' % (self.server.servername, e)
  144. log.error(response)
  145. raise
  146. if response:
  147. self._send(response)
  148. def _send(self, msg):
  149. log.debug('to %s: %s', self.client_ident(), msg)
  150. self.request.send(msg.encode('utf-8') + b'\r\n')
  151. def handle_nick(self, params):
  152. """
  153. Handle the initial setting of the user's nickname and nick changes.
  154. """
  155. nick = params
  156. # Valid nickname?
  157. if re.search('[^a-zA-Z0-9\-\[\]\'`^{}_]', nick):
  158. raise IRCError.from_name('erroneusnickname', ':%s' % nick)
  159. if self.server.clients.get(nick, None) == self:
  160. # Already registered to user
  161. return
  162. if nick in self.server.clients:
  163. # Someone else is using the nick
  164. raise IRCError.from_name('nicknameinuse', 'NICK :%s' % (nick))
  165. if not self.nick:
  166. # New connection and nick is available; register and send welcome
  167. # and MOTD.
  168. self.nick = nick
  169. self.server.clients[nick] = self
  170. response = ':%s %s %s :%s' % (self.server.servername,
  171. events.codes['welcome'], self.nick, SRV_WELCOME)
  172. self.send_queue.append(response)
  173. response = ':%s 376 %s :End of MOTD command.' % (
  174. self.server.servername, self.nick)
  175. self.send_queue.append(response)
  176. return
  177. # Nick is available. Change the nick.
  178. message = ':%s NICK :%s' % (self.client_ident(), nick)
  179. self.server.clients.pop(self.nick)
  180. self.nick = nick
  181. self.server.clients[self.nick] = self
  182. # Send a notification of the nick change to all the clients in the
  183. # channels the client is in.
  184. for channel in self.channels.values():
  185. self._send_to_others(message, channel)
  186. # Send a notification of the nick change to the client itself
  187. return message
  188. def handle_user(self, params):
  189. """
  190. Handle the USER command which identifies the user to the server.
  191. """
  192. params = params.split(' ', 3)
  193. if len(params) != 4:
  194. raise IRCError.from_name('needmoreparams',
  195. 'USER :Not enough parameters')
  196. user, mode, unused, realname = params
  197. self.user = user
  198. self.mode = mode
  199. self.realname = realname
  200. return ''
  201. def handle_ping(self, params):
  202. """
  203. Handle client PING requests to keep the connection alive.
  204. """
  205. response = ':{self.server.servername} PONG :{self.server.servername}'
  206. return response.format(**locals())
  207. def handle_join(self, params):
  208. """
  209. Handle the JOINing of a user to a channel. Valid channel names start
  210. with a # and consist of a-z, A-Z, 0-9 and/or '_'.
  211. """
  212. channel_names = params.split(' ', 1)[0] # Ignore keys
  213. for channel_name in channel_names.split(','):
  214. r_channel_name = channel_name.strip()
  215. # Valid channel name?
  216. if not re.match('^#([a-zA-Z0-9_])+$', r_channel_name):
  217. raise IRCError.from_name('nosuchchannel',
  218. '%s :No such channel' % r_channel_name)
  219. # Add user to the channel (create new channel if not exists)
  220. channel = self.server.channels.setdefault(r_channel_name,
  221. IRCChannel(r_channel_name))
  222. channel.clients.add(self)
  223. # Add channel to user's channel list
  224. self.channels[channel.name] = channel
  225. # Send the topic
  226. response_join = ':%s TOPIC %s :%s' % (channel.topic_by,
  227. channel.name, channel.topic)
  228. self.send_queue.append(response_join)
  229. # Send join message to everybody in the channel, including yourself
  230. # and send user list of the channel back to the user.
  231. response_join = ':%s JOIN :%s' % (self.client_ident(),
  232. r_channel_name)
  233. for client in channel.clients:
  234. client.send_queue.append(response_join)
  235. nicks = [client.nick for client in channel.clients]
  236. _vals = (self.server.servername, self.nick, channel.name,
  237. ' '.join(nicks))
  238. response_userlist = ':%s 353 %s = %s :%s' % _vals
  239. self.send_queue.append(response_userlist)
  240. _vals = self.server.servername, self.nick, channel.name
  241. response = ':%s 366 %s %s :End of /NAMES list' % _vals
  242. self.send_queue.append(response)
  243. def handle_privmsg(self, params):
  244. """
  245. Handle sending a private message to a user or channel.
  246. """
  247. target, sep, msg = params.partition(' ')
  248. if not msg:
  249. raise IRCError.from_name('needmoreparams',
  250. 'PRIVMSG :Not enough parameters')
  251. message = ':%s PRIVMSG %s %s' % (self.client_ident(), target, msg)
  252. if target.startswith('#') or target.startswith('$'):
  253. # Message to channel. Check if the channel exists.
  254. channel = self.server.channels.get(target)
  255. if not channel:
  256. raise IRCError.from_name('nosuchnick', 'PRIVMSG :%s' % target)
  257. if not channel.name in self.channels:
  258. # The user isn't in the channel.
  259. raise IRCError.from_name('cannotsendtochan',
  260. '%s :Cannot send to channel' % channel.name)
  261. self._send_to_others(message, channel)
  262. else:
  263. # Message to user
  264. client = self.server.clients.get(target, None)
  265. if not client:
  266. raise IRCError.from_name('nosuchnick', 'PRIVMSG :%s' % target)
  267. client.send_queue.append(message)
  268. def _send_to_others(self, message, channel):
  269. """
  270. Send the message to all clients in the specified channel except for
  271. self.
  272. """
  273. other_clients = [client for client in channel.clients
  274. if not client == self]
  275. for client in other_clients:
  276. client.send_queue.append(message)
  277. def handle_topic(self, params):
  278. """
  279. Handle a topic command.
  280. """
  281. channel_name, sep, topic = params.partition(' ')
  282. channel = self.server.channels.get(channel_name)
  283. if not channel:
  284. raise IRCError.from_name('nosuchnick', 'PRIVMSG :%s' % channel_name)
  285. if not channel.name in self.channels:
  286. # The user isn't in the channel.
  287. raise IRCError.from_name('cannotsendtochan',
  288. '%s :Cannot send to channel' % channel.name)
  289. if topic:
  290. channel.topic = topic.lstrip(':')
  291. channel.topic_by = self.nick
  292. message = ':%s TOPIC %s :%s' % (self.client_ident(), channel_name,
  293. channel.topic)
  294. return message
  295. def handle_part(self, params):
  296. """
  297. Handle a client parting from channel(s).
  298. """
  299. for pchannel in params.split(','):
  300. if pchannel.strip() in self.server.channels:
  301. # Send message to all clients in all channels user is in, and
  302. # remove the user from the channels.
  303. channel = self.server.channels.get(pchannel.strip())
  304. response = ':%s PART :%s' % (self.client_ident(), pchannel)
  305. if channel:
  306. for client in channel.clients:
  307. client.send_queue.append(response)
  308. channel.clients.remove(self)
  309. self.channels.pop(pchannel)
  310. else:
  311. _vars = self.server.servername, pchannel, pchannel
  312. response = ':%s 403 %s :%s' % _vars
  313. self.send_queue.append(response)
  314. def handle_quit(self, params):
  315. """
  316. Handle the client breaking off the connection with a QUIT command.
  317. """
  318. response = ':%s QUIT :%s' % (self.client_ident(), params.lstrip(':'))
  319. # Send quit message to all clients in all channels user is in, and
  320. # remove the user from the channels.
  321. for channel in self.channels.values():
  322. for client in channel.clients:
  323. client.send_queue.append(response)
  324. channel.clients.remove(self)
  325. def handle_dump(self, params):
  326. """
  327. Dump internal server information for debugging purposes.
  328. """
  329. print("Clients:", self.server.clients)
  330. for client in self.server.clients.values():
  331. print(" ", client)
  332. for channel in client.channels.values():
  333. print(" ", channel.name)
  334. print("Channels:", self.server.channels)
  335. for channel in self.server.channels.values():
  336. print(" ", channel.name, channel)
  337. for client in channel.clients:
  338. print(" ", client.nick, client)
  339. def client_ident(self):
  340. """
  341. Return the client identifier as included in many command replies.
  342. """
  343. return irc.client.NickMask.from_params(self.nick, self.user,
  344. self.server.servername)
  345. def finish(self):
  346. """
  347. The client conection is finished. Do some cleanup to ensure that the
  348. client doesn't linger around in any channel or the client list, in case
  349. the client didn't properly close the connection with PART and QUIT.
  350. """
  351. log.info('Client disconnected: %s', self.client_ident())
  352. response = ':%s QUIT :EOF from client' % self.client_ident()
  353. for channel in self.channels.values():
  354. if self in channel.clients:
  355. # Client is gone without properly QUITing or PARTing this
  356. # channel.
  357. for client in channel.clients:
  358. client.send_queue.append(response)
  359. channel.clients.remove(self)
  360. if self.nick:
  361. self.server.clients.pop(self.nick)
  362. log.info('Connection finished: %s', self.client_ident())
  363. def __repr__(self):
  364. """
  365. Return a user-readable description of the client
  366. """
  367. return '<%s %s!%s@%s (%s)>' % (
  368. self.__class__.__name__,
  369. self.nick,
  370. self.user,
  371. self.host[0],
  372. self.realname,
  373. )
  374. class IRCServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
  375. daemon_threads = True
  376. allow_reuse_address = True
  377. channels = {}
  378. "Existing channels (IRCChannel instances) by channel name"
  379. clients = {}
  380. "Connected clients (IRCClient instances) by nick name"
  381. def __init__(self, *args, **kwargs):
  382. self.servername = 'localhost'
  383. self.channels = {}
  384. self.clients = {}
  385. if six.PY2:
  386. socketserver.TCPServer.__init__(self, *args, **kwargs)
  387. return
  388. super().__init__(*args, **kwargs)
  389. def get_args():
  390. parser = argparse.ArgumentParser()
  391. parser.add_argument("-a", "--address", dest="listen_address",
  392. default='127.0.0.1', help="IP on which to listen")
  393. parser.add_argument("-p", "--port", dest="listen_port", default=6667,
  394. type=int, help="Port on which to listen")
  395. jaraco.logging.add_arguments(parser)
  396. return parser.parse_args()
  397. def main():
  398. options = get_args()
  399. jaraco.logging.setup(options)
  400. log.info("Starting irc.server")
  401. try:
  402. bind_address = options.listen_address, options.listen_port
  403. ircserver = IRCServer(bind_address, IRCClient)
  404. _tmpl = 'Listening on {listen_address}:{listen_port}'
  405. log.info(_tmpl.format(**vars(options)))
  406. ircserver.serve_forever()
  407. except socket.error as e:
  408. log.error(repr(e))
  409. raise SystemExit(-2)
  410. if __name__ == "__main__":
  411. main()