You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
328 lines
9.4 KiB
Python
328 lines
9.4 KiB
Python
#!/usr/bin/python
|
|
|
|
import math, socket, ssl, select, time, threading, random, string, os
|
|
from collections import deque
|
|
|
|
EOC = "\r\n"
|
|
|
|
config_ownhost = "ircd.local"
|
|
config_password = ""
|
|
config_netname = "Cryto IRC"
|
|
config_version = "circd 0.1"
|
|
config_motd = "sample.motd"
|
|
|
|
def remove_from_list(ls, val):
|
|
return [value for value in ls if value is not val]
|
|
|
|
def split_irc(message):
|
|
if ":" in message:
|
|
first, second = message.split(":", 2)
|
|
return first.rstrip().split(" ") + [second]
|
|
else:
|
|
return message.split(" ")
|
|
|
|
class autodict(dict):
|
|
# http://stackoverflow.com/a/652284
|
|
def __getitem__(self, item):
|
|
try:
|
|
return dict.__getitem__(self, item)
|
|
except KeyError:
|
|
value = self[item] = type(self)()
|
|
return value
|
|
|
|
class ircd:
|
|
channels = {}
|
|
users = {}
|
|
motd = ""
|
|
|
|
def __init__(self):
|
|
if config_motd != "":
|
|
try:
|
|
self.motd = open(config_motd, "r").read()
|
|
except IOError:
|
|
print "WARNING: Could not read MOTD file."
|
|
|
|
class listener:
|
|
ssl = False
|
|
server = None
|
|
client_list = []
|
|
client_map = {}
|
|
select_inputs = []
|
|
select_outputs = []
|
|
|
|
def __init__(self, server):
|
|
self.server = server
|
|
|
|
def start(self, interface, port, cert_path, key_path):
|
|
bindsocket = socket.socket()
|
|
bindsocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
bindsocket.bind((interface, port))
|
|
bindsocket.listen(5)
|
|
|
|
self.select_inputs = [ bindsocket ]
|
|
|
|
while self.select_inputs:
|
|
readable, writable, error = select.select(self.select_inputs, self.select_outputs, self.select_inputs)
|
|
|
|
if len(readable) > 0:
|
|
for sock in readable:
|
|
try:
|
|
if sock is bindsocket:
|
|
newsocket, fromaddr = bindsocket.accept()
|
|
remote_ip, remote_port = fromaddr
|
|
|
|
if self.ssl == True:
|
|
connstream = ssl.wrap_socket(newsocket, server_side=True, certfile=cert_path, keyfile=key_path, ssl_version=ssl.PROTOCOL_TLSv1)
|
|
else:
|
|
connstream = newsocket
|
|
|
|
new_client = client(connstream, remote_ip, server, self)
|
|
|
|
self.select_inputs.append(connstream)
|
|
self.select_outputs.append(connstream)
|
|
self.client_map[connstream.fileno()] = new_client
|
|
self.client_list.append(new_client)
|
|
else:
|
|
cur_client = self.client_map[sock.fileno()]
|
|
|
|
try:
|
|
data = sock.recv(1024)
|
|
except socket.error, (value, message):
|
|
cur_client.abort(message)
|
|
else:
|
|
if data:
|
|
cur_client.process_data(data)
|
|
else:
|
|
cur_client.end()
|
|
self.select_inputs = remove_from_list(self.select_inputs, sock)
|
|
print "NOTICE: Client disconnected"
|
|
except ssl.SSLError, err:
|
|
if err.args[0] == ssl.SSL_ERROR_WANT_READ:
|
|
select.select([sock], [], [])
|
|
elif err.args[0] == ssl.SSL_ERROR_WANT_WRITE:
|
|
select.select([], [sock], [])
|
|
else:
|
|
raise
|
|
else:
|
|
time.sleep(0.010)
|
|
|
|
class client:
|
|
buff = ""
|
|
stream = None
|
|
user = None
|
|
listener = None
|
|
ip = ""
|
|
|
|
def __init__(self, connstream, ip, server, listener):
|
|
self.ip = ip
|
|
self.stream = connstream
|
|
self.user = user(self, server)
|
|
self.listener = listener
|
|
|
|
def abort(self, reason):
|
|
# TODO: Log quit reason
|
|
try:
|
|
self.stream.shutdown(2)
|
|
self.close()
|
|
except socket.error:
|
|
pass
|
|
|
|
self.end()
|
|
self.listener.select_inputs = remove_from_list(self.listener.select_inputs, self.stream)
|
|
print "NOTICE: Client disconnected, possibly due to socket error: %s" % reason
|
|
|
|
def send_chunk(self, chunk):
|
|
#print chunk
|
|
try:
|
|
self.stream.send(chunk + EOC)
|
|
except socket.error, (value, message):
|
|
self.abort(message)
|
|
|
|
def send_global_notice(self, notice):
|
|
self.send_chunk(":%s NOTICE %s" % (config_ownhost, notice))
|
|
|
|
def send_numeric(self, numeric, notice):
|
|
self.send_chunk(":%s %s %s %s" % (config_ownhost, numeric, self.user.nickname, notice))
|
|
|
|
def send_event(self, origin, event, message):
|
|
self.send_chunk(":%s %s %s" % (origin, event, message))
|
|
|
|
def process_data(self, data):
|
|
self.buff += data
|
|
stack = self.buff.split("\n")
|
|
self.buff = stack.pop()
|
|
|
|
for chunk in stack:
|
|
print chunk
|
|
self.process_chunk(chunk.rstrip())
|
|
|
|
def process_chunk(self, chunkdata):
|
|
data = split_irc(chunkdata)
|
|
if data[0].upper() == "PING":
|
|
self.send_chunk("PONG %s" % data[1])
|
|
else:
|
|
self.user.process_data(data)
|
|
|
|
def end(self):
|
|
if self.user is not None:
|
|
self.user.end()
|
|
|
|
class channel:
|
|
presences = {}
|
|
name = ""
|
|
registered = False
|
|
|
|
def __init__(self, channelname):
|
|
self.name = channelname
|
|
|
|
class user:
|
|
client = None
|
|
server = None
|
|
registered = 0
|
|
registered_nick = False
|
|
registered_user = False
|
|
presences = {}
|
|
nickname = "*"
|
|
ident = ""
|
|
realname = ""
|
|
masked_host = ""
|
|
real_host = ""
|
|
ip = ""
|
|
|
|
def __init__(self, client, server):
|
|
self.server = server
|
|
self.client = client
|
|
self.client.send_global_notice("AUTH :*** Looking up your hostname...")
|
|
|
|
try:
|
|
hostname, aliaslist, ipaddrlist = socket.gethostbyaddr(self.client.ip)
|
|
self.client.send_global_notice("AUTH :*** Found your hostname")
|
|
except socket.herror:
|
|
hostname = self.client.ip
|
|
self.client.send_global_notice("AUTH :*** Could not find your hostname, using IP address instead")
|
|
|
|
self.real_host = hostname
|
|
self.masked_host = hostname
|
|
|
|
if config_password == "":
|
|
self.registered = 1
|
|
|
|
def __str__(self):
|
|
return "%s!%s@%s" % (self.nickname, self.ident, self.masked_host)
|
|
|
|
def process_data(self, data):
|
|
data = deque(data)
|
|
|
|
if data[0].startswith(":"):
|
|
origin = data.popleft()
|
|
else:
|
|
origin = ""
|
|
|
|
data[0] = data[0].upper()
|
|
|
|
if data[0] in ["USER", "NICK"] and self.registered == 1:
|
|
if data[0] == "USER":
|
|
if len(data) >= 5:
|
|
self.ident = data[1]
|
|
self.realname = data[4]
|
|
self.registered_user = True
|
|
self.verify_registration()
|
|
else:
|
|
self.client.send_numeric("461", "USER :Not enough parameters.")
|
|
elif data[0] == "NICK":
|
|
if len(data) >= 2:
|
|
if data[1] not in self.server.users:
|
|
self.nickname = data[1]
|
|
self.registered_nick = True
|
|
self.verify_registration()
|
|
else:
|
|
self.client.send_numeric("433", "%s :Nickname is already in use." % data[1])
|
|
else:
|
|
self.client.send_numeric("461", "NICK :Not enough parameters.")
|
|
elif self.registered == 2 and data[0] == "PONG":
|
|
if data[1] == self.challenge:
|
|
self.finish_registration()
|
|
elif self.registered < 2:
|
|
self.client.send_numeric("451", "%s :You have not registered." % data[0])
|
|
elif self.registered < 3:
|
|
self.client.send_numeric("451", "%s :You have not completed the challenge PING." % data[0])
|
|
else:
|
|
if data[0] == "LUSERS":
|
|
self.send_lusers()
|
|
elif data[0] == "JOIN":
|
|
self.join_channel(data[1])
|
|
else:
|
|
self.client.send_numeric("421", "%s :Unknown command." % data[0])
|
|
|
|
def verify_registration(self):
|
|
if self.registered_nick == True and self.registered_user == True:
|
|
self.registered = 2
|
|
print "Client %s registered from IP %s, sending challenge string." % (self.nickname, self.client.ip)
|
|
self.send_challenge()
|
|
|
|
def send_challenge(self):
|
|
self.challenge = ''.join(random.choice(string.ascii_uppercase + string.digits) for i in xrange(8))
|
|
self.client.send_chunk("PING :%s" % self.challenge)
|
|
|
|
def finish_registration(self):
|
|
self.registered = 3
|
|
self.server.users[self.nickname] = self
|
|
self.client.send_numeric("001", ":Welcome to %s, %s!%s@%s" % (config_netname, self.nickname, self.ident, self.real_host))
|
|
self.client.send_numeric("002", ":Your host is %s, running %s." % (config_ownhost, config_version))
|
|
self.client.send_numeric("003", ":This server has been running since unknown.")
|
|
self.client.send_numeric("004", ":%s %s %s %s" % (config_ownhost, config_version, "", ""))
|
|
self.send_lusers()
|
|
self.send_motd()
|
|
|
|
def send_lusers(self):
|
|
self.client.send_numeric("251", ":There are %d users and 0 invisible on 1 server." % len(self.server.users))
|
|
self.client.send_numeric("252", "%d :operator(s) online" % 0)
|
|
self.client.send_numeric("254", "%d :channel(s) formed" % 0)
|
|
self.client.send_numeric("255", ":I have %d clients and 1 server." % len(self.server.users)) # TODO: Sum all clients of all listenersm rather than taking the usercount.
|
|
|
|
def send_motd(self):
|
|
if server.motd == "":
|
|
self.client.send_numeric("422", ":No MOTD was set.")
|
|
else:
|
|
self.client.send_numeric("375", ":- %s Message of the day -" % config_ownhost)
|
|
|
|
for line in server.motd.rstrip().split("\n"):
|
|
self.client.send_numeric("372", ":- %s" % line.rstrip())
|
|
|
|
self.client.send_numeric("375", ":End of MOTD for %s." % config_ownhost)
|
|
|
|
def join_channel(self, channelname):
|
|
if channelname not in self.server.channels:
|
|
self.server.channels[channelname] = channel(channelname)
|
|
|
|
targetchannel = self.server.channels[channelname]
|
|
|
|
if self.nickname not in targetchannel.presences:
|
|
newpresence = presence(targetchannel, self)
|
|
self.server.channels[channelname].presences[self.nickname] = newpresence
|
|
self.presences[channelname] = newpresence
|
|
self.client.send_event(self, "JOIN", ":%s" % channelname)
|
|
self.client.send_numeric("353", "= %s :%s" % (channelname, "@hai blah"))
|
|
self.client.send_numeric("366", "%s :End of userlist." % channelname)
|
|
print self.server.channels[channelname].presences
|
|
print self.presences[channelname]
|
|
|
|
def end(self):
|
|
del self.server.users[self.nickname]
|
|
|
|
class presence:
|
|
user = None
|
|
channel = None
|
|
status = ""
|
|
joined = 0
|
|
|
|
def __init__(self, targetchannel, targetuser):
|
|
self.user = targetuser
|
|
self.channel = targetchannel
|
|
self.status = ""
|
|
|
|
server = ircd()
|
|
|
|
l = listener(server)
|
|
l.start("0.0.0.0", 6667, "sample.cert", "sample.key")
|