2016-07-14 10:37:07 +00:00
import sys
2016-07-14 13:38:28 +00:00
import traceback
2016-07-14 10:37:07 +00:00
import socket
import select
import time
import re
import hashlib
from helpers import logHelper as log
from objects import glob
from helpers import chatHelper as chat
2016-07-14 13:38:28 +00:00
import raven
2016-07-14 10:37:07 +00:00
class Client :
"""
IRC Client object
"""
__linesep_regexp = re . compile ( r " \ r? \ n " )
def __init__ ( self , server , sock ) :
"""
Initialize a Client object
server - - server object
sock - - socket connection object
"""
self . __timestamp = time . time ( )
self . __readbuffer = " "
self . __writebuffer = " "
self . __sentPing = False
self . __handleCommand = self . passHandler
self . server = server
self . socket = sock
( self . ip , self . port ) = sock . getpeername ( )
self . username = " "
self . supposedUsername = " "
self . joinedChannels = [ ]
def messageChannel ( self , channel , command , message , includeSelf = False ) :
line = " : {} {} " . format ( command , message )
for _ , value in self . server . clients . items ( ) :
if channel in value . joinedChannels and ( value != self or includeSelf ) :
value . message ( line )
def message ( self , msg ) :
"""
Add a message ( basic string ) to client buffer .
This is the lowest possible level .
msg - - message to add
"""
self . __writebuffer + = msg + " \r \n "
def writeBufferSize ( self ) :
"""
Return this client ' s write buffer size
return - - write buffer size
"""
return len ( self . __writebuffer )
def reply ( self , msg ) :
"""
Add an IRC - like message to client buffer .
msg - - message ( without IRC stuff )
"""
self . message ( " : {} {} " . format ( self . server . host , msg ) )
def replyCode ( self , code , message , nickname = " " , channel = " " ) :
"""
Add an IRC - like message to client buffer with code
code - - response code
message - - response message
nickname - - receiver nickname
channel - - optional
"""
if nickname == " " :
nickname = self . username
if channel != " " :
channel = " " + channel
self . reply ( " {code:03d} {nickname} {channel} : {message} " . format ( code = code , nickname = nickname , channel = channel , message = message ) )
def reply403 ( self , channel ) :
"""
Add a 403 reply ( no such channel ) to client buffer .
channel - - meh
"""
self . replyCode ( 403 , " {} :No such channel " . format ( channel ) )
def reply461 ( self , command ) :
"""
Add a 461 reply ( not enough parameters ) to client buffer
command - - command that had not enough parameters
"""
self . replyCode ( 403 , " {} :Not enough parameters " . format ( command ) )
def disconnect ( self , quitmsg = " Client quit " , callLogout = True ) :
"""
Disconnects this client from the IRC server
quitmsg - - IRC quit message . Default : ' Client quit '
callLogout - - if True , call logoutEvent on bancho
"""
# Send error to client and close socket
self . message ( " ERROR : {} " . format ( quitmsg ) )
self . socket . close ( )
log . info ( " [IRC] Disconnected connection from {} : {} ( {} ) " . format ( self . ip , self . port , quitmsg ) )
# Remove socket from server
self . server . removeClient ( self , quitmsg )
# Bancho logout
if callLogout == True :
chat . IRCDisconnect ( self . username )
def readSocket ( self ) :
""" Read data coming from this client socket """
try :
# Try to read incoming data from socket
data = self . socket . recv ( 2 * * 10 )
log . debug ( " [IRC] [ {} : {} ] -> {} " . format ( self . ip , self . port , data ) )
quitmsg = " EOT "
except socket . error as x :
# Error while reading data, this client will be disconnected
data = " "
quitmsg = x
if data :
# Parse received data if needed
self . __readbuffer + = data . decode ( " latin_1 " )
self . parseBuffer ( )
self . __timestamp = time . time ( )
self . __sentPing = False
else :
# No data, disconnect this socket
self . disconnect ( quitmsg )
def parseBuffer ( self ) :
""" Parse self.__readbuffer, get command, arguments and call its handler """
# Get lines from buffer
lines = self . __linesep_regexp . split ( self . __readbuffer )
self . __readbuffer = lines [ - 1 ]
lines = lines [ : - 1 ]
# Process every line
for line in lines :
if not line :
# Empty line. Ignore.
continue
# Get arguments
x = line . split ( " " , 1 )
# Command is the first argument, always uppercase
command = x [ 0 ] . upper ( )
if len ( x ) == 1 :
# Command only, no arguments
arguments = [ ]
else :
# We have some arguments
# Weird sorcery
if len ( x [ 1 ] ) > 0 and x [ 1 ] [ 0 ] == " : " :
arguments = [ x [ 1 ] [ 1 : ] ]
else :
y = x [ 1 ] . split ( " : " , 1 )
arguments = y [ 0 ] . split ( )
if len ( y ) == 2 :
arguments . append ( y [ 1 ] )
# Handle command with its arguments
self . __handleCommand ( command , arguments )
def writeSocket ( self ) :
""" Write buffer to socket """
try :
sent = self . socket . send ( self . __writebuffer . encode ( ) )
log . debug ( " [IRC] [ {} : {} ] <- {} " . format ( self . ip , self . port , self . __writebuffer [ : sent ] ) )
self . __writebuffer = self . __writebuffer [ sent : ]
except socket . error as x :
self . disconnect ( x )
def checkAlive ( self ) :
""" Check if this client is still connected """
now = time . time ( )
if self . __timestamp + 180 < now :
self . disconnect ( " ping timeout " )
return
if not self . __sentPing and self . __timestamp + 90 < now :
if self . __handleCommand == self . mainHandler :
# Registered.
self . message ( " PING : {} " . format ( self . server . host ) )
self . __sentPing = True
else :
# Not registered.
self . disconnect ( " ping timeout " )
def sendLusers ( self ) :
""" Send lusers response to this client """
self . replyCode ( 251 , " There are {} users and 0 services on 1 server " . format ( len ( glob . tokens . tokens ) ) )
def sendMotd ( self ) :
""" Send MOTD to this client """
self . replyCode ( 375 , " - {} Message of the day - " . format ( self . server . host ) )
if len ( self . server . motd ) == 0 :
self . replyCode ( 422 , " MOTD File is missing " )
else :
for i in self . server . motd :
self . replyCode ( 372 , " - {} " . format ( i ) )
self . replyCode ( 376 , " End of MOTD command " )
""" """ """
HANDLERS
""" """ """
def dummyHandler ( self , command , arguments ) :
pass
def passHandler ( self , command , arguments ) :
""" PASS command handler """
if command == " PASS " :
if len ( arguments ) == 0 :
self . reply461 ( " PASS " )
else :
# IRC token check
m = hashlib . md5 ( )
m . update ( arguments [ 0 ] . encode ( " utf-8 " ) )
tokenHash = m . hexdigest ( )
supposedUsername = glob . db . fetch ( " SELECT users.username FROM users LEFT JOIN irc_tokens ON users.id = irc_tokens.userid WHERE irc_tokens.token = %s LIMIT 1 " , [ tokenHash ] )
if supposedUsername :
self . supposedUsername = supposedUsername [ " username " ]
self . __handleCommand = self . registerHandler
else :
# Wrong IRC Token
self . reply ( " 464 :Password incorrect " )
elif command == " QUIT " :
self . disconnect ( )
def registerHandler ( self , command , arguments ) :
""" NICK and USER commands handler """
if command == " NICK " :
if len ( arguments ) < 1 :
self . reply ( " 431 :No nickname given " )
return
nick = arguments [ 0 ]
# Make sure this is the first time we set our nickname
if self . username != " " :
self . reply ( " 432 * %s :Erroneous nickname " % nick )
return
# Make sure the IRC token was correct:
if nick . lower ( ) != self . supposedUsername . lower ( ) :
self . reply ( " 464 :Password incorrect " )
return
# Make sure we are not connected to Bancho
token = glob . tokens . getTokenFromUsername ( nick )
if token != None :
self . reply ( " 433 * {} :Nickname is already in use " . format ( nick ) )
return
# Make sure we are not already connected from IRC with that name
for _ , value in self . server . clients . items ( ) :
if value . username == self . username and value != self :
self . reply ( " 433 * {} :Nickname is already in use " . format ( nick ) )
return
# Everything seems fine, set username (nickname)
self . username = nick
elif command == " USER " :
# Ignore USER command, we use nickname only
return
elif command == " QUIT " :
# Disconnect if we have received a QUIT command
self . disconnect ( )
return
else :
# Ignore any other command while logging in
return
# If we now have a valid username, connect to bancho and send IRC welcome stuff
if self . username != " " :
# Bancho connection
chat . IRCConnect ( self . username )
# IRC reply
self . replyCode ( 1 , " Welcome to the Internet Relay Network " )
self . replyCode ( 2 , " Your host is {} , running version pep.py- {} " . format ( self . server . host , glob . VERSION ) )
self . replyCode ( 3 , " This server was created since the beginning " )
self . replyCode ( 4 , " {} pep.py- {} o o " . format ( self . server . host , glob . VERSION ) )
self . sendLusers ( )
self . sendMotd ( )
self . __handleCommand = self . mainHandler
def quitHandler ( self , command , arguments ) :
""" QUIT command handler """
self . disconnect ( self . username if len ( arguments ) < 1 else arguments [ 0 ] )
def joinHandler ( self , command , arguments ) :
""" JOIN command handler """
if len ( arguments ) < 1 :
self . reply461 ( " JOIN " )
return
# Get bancho token object
token = glob . tokens . getTokenFromUsername ( self . username )
if token == None :
return
# TODO: Part all channels
if arguments [ 0 ] == " 0 " :
return
''' for (channelname, channel) in self.channels.items():
self . message_channel ( channel , " PART " , channelname , True )
self . channel_log ( channel , " left " , meta = True )
server . remove_member_from_channel ( self , channelname )
self . channels = { }
return '''
# Get channels to join list
channels = arguments [ 0 ] . split ( " , " )
for channel in channels :
# Make sure we are not already in that channel
# (we already check this bancho-side, but we need to do it
# also here k maron)
if channel . lower ( ) in token . joinedChannels :
continue
# Attempt to join the channel
response = chat . IRCJoinChannel ( self . username , channel )
if response == 0 :
# Joined successfully
self . joinedChannels . append ( channel )
# Let everyone in this channel know that we've joined
self . messageChannel ( channel , " {} JOIN " . format ( self . username ) , channel , True )
# Send channel description (topic)
description = glob . channels . channels [ channel ] . description
if description == " " :
self . replyCode ( 331 , " No topic is set " , channel = channel )
else :
self . replyCode ( 332 , description , channel = channel )
# Build connected users list
users = glob . channels . channels [ channel ] . getConnectedUsers ( ) [ : ]
usernames = [ ]
for user in users :
token = glob . tokens . getTokenFromUserID ( user )
if token == None :
continue
usernames . append ( token . username )
usernames = " " . join ( usernames )
# Send IRC users lis
self . replyCode ( 353 , usernames , channel = " = {} " . format ( channel ) )
self . replyCode ( 366 , " End of NAMES list " , channel = channel )
elif response == 403 :
# Channel doesn't exist (or no read permissions)
self . reply403 ( channel )
continue
def partHandler ( self , command , arguments ) :
""" PART command handler """
if len ( arguments ) < 1 :
self . reply461 ( " PART " )
return
# Get bancho token object
token = glob . tokens . getTokenFromUsername ( self . username )
if token == None :
return
# Get channels to part list
channels = arguments [ 0 ] . split ( " , " )
for channel in channels :
# Make sure we in that channel
# (we already check this bancho-side, but we need to do it
# also here k maron)
if channel . lower ( ) not in token . joinedChannels :
continue
# Attempt to part the channel
response = chat . IRCPartChannel ( self . username , channel )
if response == 0 :
# No errors, remove channel from joinedChannels
self . joinedChannels . remove ( channel )
elif response == 403 :
self . reply403 ( channel )
elif response == 442 :
self . replyCode ( 442 , " You ' re not on that channel " , channel = channel )
def noticePrivmsgHandler ( self , command , arguments ) :
""" NOTICE and PRIVMSG commands handler (same syntax) """
# Syntax check
if len ( arguments ) == 0 :
self . replyCode ( 411 , " No recipient given ( {} ) " . format ( command ) )
return
if len ( arguments ) == 1 :
self . replyCode ( 412 , " No text to send " )
return
recipient = arguments [ 0 ]
message = arguments [ 1 ]
# Send the message to bancho and reply
response = chat . sendMessage ( self . username , recipient , message , toIRC = False )
if response == 404 :
self . replyCode ( 404 , " Cannot send to channel " , channel = recipient )
return
elif response == 403 :
self . replyCode ( 403 , " No such channel " , channel = recipient )
return
elif response == 401 :
self . replyCode ( 401 , " No such nick/channel " , channel = recipient )
return
# Send the message to IRC and bancho
if recipient . startswith ( " # " ) :
# Public message (IRC)
if recipient not in glob . channels . channels :
self . replyCode ( 401 , " No such nick/channel " , channel = recipient )
return
for _ , value in self . server . clients . items ( ) :
if recipient in value . joinedChannels and value != self :
value . message ( " : {} PRIVMSG {} : {} " . format ( self . username , recipient , message ) )
#self.messageChannel(recipient, command, "{} :{}".format(recipient, message))
else :
# Private message (IRC)
for _ , value in self . server . clients . items ( ) :
if value . username == recipient :
value . message ( " : {} PRIVMSG {} : {} " . format ( self . username , recipient , message ) )
def motdHandler ( self , command , arguments ) :
""" MOTD command handler """
self . sendMotd ( )
def lusersHandler ( self , command , arguments ) :
""" LUSERS command handler """
self . sendLusers ( )
def pingHandler ( self , command , arguments ) :
""" PING command handler """
if len ( arguments ) < 1 :
self . replyCode ( 409 , " No origin specified " )
return
self . reply ( " PONG {} : {} " . format ( self . server . host , arguments [ 0 ] ) )
def pongHandler ( self , command , arguments ) :
""" (fake) PONG command handler """
pass
def mainHandler ( self , command , arguments ) :
""" Handler for post-login commands """
handlers = {
#"AWAY": away_handler,
#"ISON": ison_handler,
" JOIN " : self . joinHandler ,
#"LIST": list_handler,
" LUSERS " : self . lusersHandler ,
#"MODE": mode_handler,
" MOTD " : self . motdHandler ,
#"NICK": nick_handler,
#"NOTICE": notice_and_privmsg_handler,
" PART " : self . partHandler ,
" PING " : self . pingHandler ,
" PONG " : self . pongHandler ,
" PRIVMSG " : self . noticePrivmsgHandler ,
" QUIT " : self . quitHandler ,
#"TOPIC": topic_handler,
#"WALLOPS": wallops_handler,
#"WHO": who_handler,
#"WHOIS": whois_handler,
" USER " : self . dummyHandler ,
}
try :
handlers [ command ] ( command , arguments )
except KeyError :
self . replyCode ( 421 , " Unknown command ( {} ) " . format ( command ) )
class Server :
def __init__ ( self , port ) :
self . host = socket . getfqdn ( " 127.0.0.1 " ) [ : 63 ]
self . port = port
self . clients = { } # Socket --> Client instance.
self . motd = [ " Welcome to pep.py ' s embedded IRC server! " , " This is a VERY simple IRC server and it ' s still in beta. " , " Expect things to crash and not work as expected :( " ]
def forceDisconnection ( self , username ) :
"""
Disconnect someone from IRC if connected
username - - victim
"""
for _ , value in self . clients . items ( ) :
if value . username == username :
value . disconnect ( callLogout = False )
break # or dictionary changes size during iteration
def banchoJoinChannel ( self , username , channel ) :
"""
Let every IRC client connected to a specific client know that ' username ' joined the channel from bancho
username - - username of bancho user
channel - - joined channel name
"""
for _ , value in self . clients . items ( ) :
if channel in value . joinedChannels :
value . message ( " : {} JOIN {} " . format ( username , channel ) )
def banchoPartChannel ( self , username , channel ) :
"""
Let every IRC client connected to a specific client know that ' username ' parted the channel from bancho
username - - username of bancho user
channel - - joined channel name
"""
for _ , value in self . clients . items ( ) :
if channel in value . joinedChannels :
value . message ( " : {} PART {} " . format ( username , channel ) )
def banchoMessage ( self , fro , to , message ) :
"""
Send a message to IRC when someone sends it from bancho
fro - - sender username
to - - receiver username
message - - text of the message
"""
if to . startswith ( " # " ) :
# Public message
for _ , value in self . clients . items ( ) :
if to in value . joinedChannels and value . username != fro :
value . message ( " : {} PRIVMSG {} : {} " . format ( fro , to , message ) )
else :
# Private message
for _ , value in self . clients . items ( ) :
if value . username == to and value . username != fro :
value . message ( " : {} PRIVMSG {} : {} " . format ( fro , to , message ) )
def removeClient ( self , client , quitmsg ) :
"""
Remove a client from connected clients
client - - client object
quitmsg - - QUIT argument , useless atm
"""
if client . socket in self . clients :
del self . clients [ client . socket ]
def start ( self ) :
""" Start IRC server main loop """
2016-07-14 13:38:28 +00:00
# Sentry
if glob . sentry == True :
sentryClient = raven . Client ( glob . conf . config [ " sentry " ] [ " ircdns " ] )
2016-07-14 10:37:07 +00:00
serversocket = socket . socket ( socket . AF_INET , socket . SOCK_STREAM )
serversocket . setsockopt ( socket . SOL_SOCKET , socket . SO_REUSEADDR , 1 )
try :
2016-07-14 16:01:55 +00:00
serversocket . bind ( ( " 0.0.0.0 " , self . port ) )
2016-07-14 10:37:07 +00:00
except socket . error as e :
log . error ( " [IRC] Could not bind port {} : {} " . format ( self . port , e ) )
sys . exit ( 1 )
serversocket . listen ( 5 )
lastAliveCheck = time . time ( )
# Main server loop
2016-07-16 21:23:14 +00:00
while True :
try :
2016-07-14 13:38:28 +00:00
( iwtd , owtd , ewtd ) = select . select (
[ serversocket ] + [ x . socket for x in self . clients . values ( ) ] ,
[ x . socket for x in self . clients . values ( )
if x . writeBufferSize ( ) > 0 ] ,
[ ] ,
2 )
# Handle incoming connections
for x in iwtd :
if x in self . clients :
self . clients [ x ] . readSocket ( )
else :
( conn , addr ) = x . accept ( )
2016-07-14 10:37:07 +00:00
try :
2016-07-14 13:38:28 +00:00
self . clients [ conn ] = Client ( self , conn )
log . info ( " [IRC] Accepted connection from {} : {} " . format ( addr [ 0 ] , addr [ 1 ] ) )
except socket . error as e :
try :
conn . close ( )
except :
pass
# Handle outgoing connections
for x in owtd :
if x in self . clients : # client may have been disconnected
self . clients [ x ] . writeSocket ( )
# Make sure all IRC clients are still connected
now = time . time ( )
if lastAliveCheck + 10 < now :
for client in list ( self . clients . values ( ) ) :
client . checkAlive ( )
lastAliveCheck = now
2016-07-16 21:23:14 +00:00
except :
log . error ( " [IRC] Unknown error! \n ``` \n {} \n {} ``` " . format ( sys . exc_info ( ) , traceback . format_exc ( ) ) )
if glob . sentry == True :
sentryClient . captureException ( )
2016-07-14 10:37:07 +00:00
def main ( port = 6667 ) :
glob . ircServer = Server ( port )
glob . ircServer . start ( )