ref: 34ba346d0d82fa4e34e953f872d62bfc5c375df0
author: mkf <mkf@cloud9p.org>
date: Sun Dec 17 09:23:11 EST 2023
import sources
--- /dev/null
+++ b/ircd.py
@@ -1,0 +1,325 @@
+#!/usr/bin/env python
+# a basic ircd
+# JOIN, PART
+import socket
+import time
+from select import select
+from enum import Enum
+
+defaults = {}
+defaults['hostname'] = 'localhost'
+defaults['pingtime'] = 250
+defaults['name'] = 'ircd.py'
+defaults['version'] = '1.0'
+defaults['host'] = '' # aka 0.0.0.0
+defaults['port'] = 6667
+defaults['motd'] = 'Hi.'
+defaults['encoding'] = 'utf-8'
+
+# codes
+class C(Enum):
+ WELCOME = 1
+
+ MOTD = 372
+ MOTDSTART = 375
+ ENDOFMOTD = 376
+
+
+ # errors
+ NOSUCHNICK = 401
+
+ UNKNOWNCOMMAND = 421
+
+ ERRONEUSNICKNAME = 431
+ NONICKNAMEGIVEN = 432
+ NICKNAMEINUSE = 433
+
+ NOTONCHANNEL = 442
+
+ #NOTREGISTERED = 451
+
+ NEEDMOREPARAMS = 461
+ ALREADYREGISTERED = 462
+
+Cmsg = {}
+Cmsg[C.WELCOME] = 'Welcome.'
+Cmsg[C.MOTDSTART] = 'Message of the day is:'
+Cmsg[C.ENDOFMOTD] = 'End of the /MOTD'
+Cmsg[C.NOSUCHNICK] = 'No such nickname or channel'
+Cmsg[C.UNKNOWNCOMMAND] = 'Unknown command'
+Cmsg[C.ERRONEUSNICKNAME] = 'Invalid nick'
+Cmsg[C.NONICKNAMEGIVEN] = 'No nickname were given'
+Cmsg[C.NICKNAMEINUSE] = 'Nickname is already in use'
+Cmsg[C.NOTONCHANNEL] = 'No such channel'
+Cmsg[C.NEEDMOREPARAMS] = 'Not enough parameters'
+Cmsg[C.ALREADYREGISTERED] = 'Client is already registered'
+
+class Client(object):
+ def __init__(self, server, sock, addr):
+ self.sock = sock
+ self.addr = addr
+ self.server = server
+
+ self.ip = self.addr[0]
+ self.port = self.addr[1]
+
+ self.nick = ''
+ self.user = ''
+ self.real = ''
+ self.registered = False
+
+ self.ping = 0
+
+ self.sendbuf = []
+ self.recvbuf = []
+
+ print('New client', self.ip, self.port, 'has connected')
+
+ def send(self, msg):
+ self.sendbuf.append(msg)
+
+ def _reply(self, cmd, msg):
+ self.send(':{} {} {} {}\r\n'.format(self.server.hostname, str(cmd).rjust(3, '0'), self.nick, msg))
+
+ def reply(self, cmd):
+ self._reply(cmd.value, Cmsg[cmd])
+
+ def register(self):
+ if(self.nick == '' or self.user == '' or self.registered != False):
+ return False
+
+ self.registered = True
+ print('Client', self.ip, 'has registed with nick', self.nick, 'and user', self.user)
+ self.reply(C.WELCOME)
+ self.cmd_motd([])
+ return True
+
+ def cmd_handler(self, cmdl):
+ print('cmdl:', cmdl)
+
+ if(len(cmdl) != 0 and cmdl[-1] == '\r'):
+ cmdl = cmdl[:-1]
+
+ cmdl = cmdl.split(' ')
+ cmd = cmdl[0]
+ if(len(cmdl) > 0):
+ args = cmdl[1:]
+ else:
+ args = ''
+
+ # fast exit
+ if(cmd == '\r' or cmd == '\n' or cmd == '\r\n' or cmd.isspace() or cmd == ''):
+ return
+
+ print('cmd:', cmd)
+ print('args:', args)
+
+ match(cmd.upper()):
+ case 'NICK':
+ self.cmd_nick(args)
+ case 'USER':
+ self.cmd_user(args)
+ case 'PING':
+ self.cmd_ping(args)
+ case 'PRIVMSG':
+ self.cmd_privmsg(args)
+ case 'MOTD':
+ self.cmd_motd(args)
+ case 'QUIT':
+ self.cmd_quit(args)
+ case 'JOIN':
+ self.cmd_join(args)
+ case 'PART':
+ self.cmd_part(args)
+ case _:
+ self._reply(C.UNKNOWNCOMMAND.value, Cmsg[C.UNKNOWNCOMMAND] + ' ' + cmd)
+
+ def validnick(self, nick):
+ return nick.isalpha()
+
+ def cmd_nick(self, nick):
+ if(len(nick) < 1 or nick[0] == ''):
+ self.reply(C.NONICKNAMEGIVEN)
+
+ elif(self.validnick(nick[0]) == False):
+ self.reply(C.ERRONEUSNICKNAME)
+
+ elif(self.server.findnick(nick[0]) != None):
+ self.reply(C.NICKNAMEINUSE)
+ else:
+ self.nick = nick[0]
+
+ self.register()
+
+ def cmd_user(self, user):
+ if(self.user != ''):
+ self.reply(C.ALREADYREGISTERED)
+ else:
+ self.user = user[0]
+
+ self.register()
+
+ def cmd_ping(self, cmd):
+ id = cmd[0]
+ self.send('PONG {}\n'.format(id))
+
+ def cmd_quit(self, cmd):
+ # XXX self.broadcast()
+ self.sock.send(bytes('ERROR :closing link\r\n', 'utf-8'))
+ self.sock.close()
+ self.server.clients.remove(self)
+
+ def cmd_privmsg(self, cmd):
+ if(len(cmd) < 2):
+ self.reply(C.NEEDMOREPARAMS)
+
+ dst = cmd[0]
+ msg = cmd[1:]
+
+ tmsg = ''
+ for i in msg:
+ tmsg += (' ' + i)
+
+ u = self.server.findnick(dst)
+ ch = self.server.findchan(dst)
+ if(dst[0] == '#'):
+ if(ch == None):
+ self.reply(C.NOSUCHNICK)
+ else:
+ ch.bcast(':%s!%s@%s PRIVMSG %s%s\r\n' % (self.nick, self.user, self.ip, dst, tmsg))
+
+ else:
+ if(u == None):
+ self.reply(C.NOSUCHNICK)
+ else:
+ u.send(':%s!%s@%s PRIVMSG %s%s\r\n' % (self.nick, self.user, self.ip, dst, tmsg))
+ #u.send(':%s PRIVMSG %s%s\r\n' % (self.nick, nick, tmsg))
+
+ print('privmsg(): ', ':%s!%s@%s PRIVMSG %s%s\r\n' % (self.nick, self.user, self.ip, dst, tmsg))
+
+ def cmd_motd(self, cmd):
+ self.reply(C.MOTDSTART)
+ self._reply(C.MOTD.value, self.server.motd)
+ self.reply(C.ENDOFMOTD)
+
+ def cmd_join(self, cmd):
+ if(len(cmd) < 1):
+ self.reply(C.NEEDMOREPARAMS)
+
+ chan = cmd[0]
+ channel = self.server.findchan(chan)
+
+ if(channel == None):
+ channel = self.server.chans.append(Chan(chan, self))
+ else:
+ channel.clients.append(self)
+
+ def cmd_part(self, cmd):
+ if(len(cmd) < 1):
+ self.reply(C.NEEDMOREPARAMS)
+ chan = cmd[0]
+ channel = self.server.findchan(chan)
+
+ if(not self in channel.clients):
+ self.reply(C.NOTONCHANNEL)
+ else:
+ channel.clients.remove(self)
+
+ def fileno(self):
+ return self.sock.fileno()
+
+class Chan(object):
+ def __init__(self, name, client):
+ self.name = name
+ self.clients = [client]
+ self.topic = ''
+ self.ctime = int(time.time())
+
+ def __repr__(self):
+ return '<Channel ' + self.name + '>'
+
+ def bcast(self, msg):
+ for c in self.clients:
+ c.sendbuf.append(msg)
+
+ def send(self, client, msg):
+ for c in self.clients:
+ if(c == client):
+ return
+ c.sendbuf.append(msg)
+
+
+class Server(socket.socket):
+ def __init__(self, cfg):
+ super(Server, self).__init__(socket.AF_INET, socket.SOCK_STREAM)
+ self.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+
+ self.chans = []
+ self.clients = []
+
+ self.name = cfg['name']
+ self.motd = cfg['motd']
+
+ self.hostname = cfg['hostname']
+ self.pingtime = cfg['pingtime']
+
+ self.host = cfg['host']
+ self.port = cfg['port']
+
+ self.encoding = cfg['encoding']
+
+
+ def findnick(self, nick):
+ for i in self.clients:
+ if(i.nick == nick):
+ return i
+ return None
+
+ def findchan(self, chan):
+ for i in self.chans:
+ if(i.name == chan):
+ return i
+ return None
+
+ def main(self):
+ self.bind((self.host, self.port))
+ self.listen()
+
+ while True:
+ sendu = [c for c in self.clients if len(c.sendbuf) > 0]
+ read, write, err = select([self] + self.clients, sendu, self.clients) # why self + self.clients
+
+ # new clients
+ if self in read:
+ sock, address = self.accept()
+ self.clients.append(Client(self, sock, address))
+ continue
+
+ for c in read:
+ recv = c.sock.recv(4096)
+ recv = recv.decode()
+ recv = recv.strip('\r')
+
+ print('client:', c.ip, 'recv:', recv)
+ for i in recv.split('\n'):
+ c.recvbuf.append(i)
+
+ print('c.recvbuf:', c.recvbuf)
+
+ while len(c.recvbuf) > 0:
+ c.cmd_handler(c.recvbuf.pop(0))
+
+ # send messages to each user
+ for c in write:
+ # send unsent parts of message each time
+ while len(c.sendbuf) > 0:
+ msg = bytes(c.sendbuf.pop(0), self.encoding)
+ c.sock.send(msg)
+
+
+def main():
+ srv = Server(defaults)
+ srv.main()
+
+if __name__ == '__main__':
+ main()