638 lines
18 KiB
Python
638 lines
18 KiB
Python
import sys
|
|
import traceback
|
|
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
|
|
import raven
|
|
|
|
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"""
|
|
# Sentry
|
|
if glob.sentry == True:
|
|
sentryClient = raven.Client(glob.conf.config["sentry"]["ircdns"])
|
|
|
|
serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
serversocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
try:
|
|
serversocket.bind(("0.0.0.0", self.port))
|
|
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
|
|
try:
|
|
while True:
|
|
(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()
|
|
try:
|
|
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
|
|
except:
|
|
log.error("[IRC] Unknown error!\n```\n{}\n{}```".format(sys.exc_info(), traceback.format_exc()))
|
|
if glob.sentry == True:
|
|
sentryClient.captureException()
|
|
|
|
def main(port=6667):
|
|
glob.ircServer = Server(port)
|
|
glob.ircServer.start()
|