monolith/threshold

534 lines
18 KiB
Python
Executable File

#!/usr/bin/env python
from twisted.internet import reactor
from twisted.internet.ssl import DefaultOpenSSLContextFactory
from twisted.internet.protocol import Protocol, Factory
from twisted.internet.endpoints import SSL4ClientEndpoint, TCP4ClientEndpoint, connectProtocol
from twisted.words.protocols.irc import IRCClient
from json import load, dump, loads
from sys import exit
listener = None
connections = {}
IRCPool = {}
def log(data):
print("[LOG]", data)
def debug(data):
print("[DEBUG]", data)
def warn(data):
print("[WARNING]", data)
def error(data):
print("[ERROR]", data)
exit(1)
def sendData(addr, data):
connections[addr].send(data)
def sendSuccess(addr, data):
sendData(addr, "[y] " + data)
def sendFailure(addr, data):
sendData(addr, "[n] " + data)
def sendInfo(addr, data):
sendData(addr, "[i] " + data)
class IRCBot(IRCClient):
def __init__(self, name):
self.name = name
instance = pool[name]
self.nickname = instance["nickname"]
self.realname = instance["realname"]
self.username = instance["username"]
self.userinfo = instance["userinfo"]
self.fingerReply = instance["finger"]
self.versionName = instance["version"]
self.versionNum = None
self.versionEnv = None
self.sourceURL = instance["source"]
self._who = {}
self._getWho = {}
self.wholist = {}
self.authtype = instance["authtype"]
if self.authtype == "ns":
self.authpass = instance["password"]
self.authentity = instance["authentity"]
else:
self.password = instance["password"]
def refresh(self):
instance = pool[name]
if not instance["nickname"] == self.nickname:
self.nickname = instance["nickname"]
self.setNick(self.nickname)
self.userinfo = instance["userinfo"]
self.fingerReply = instance["finger"]
self.versionName = instance["version"]
self.versionNum = None
self.versionEnv = None
self.sourceURL = instance["source"]
def sanitize(self, user):
user = user.replace("!", "")
user = user.replace("~", "")
user = user.replace("&", "")
user = user.replace("@", "")
user = user.replace("%", "")
user = user.replace("+", "")
return user
def setNick(self, nickname):
self._attemptedNick = nickname
self.sendLine("NICK %s" % nickname)
self.nickname = nickname
def alterCollidedNick(self, nickname):
newnick = nickname + "_"
return newnick
def irc_ERR_NICKNAMEINUSE(self, prefix, params):
self._attemptedNick = self.alterCollidedNick(self._attemptedNick)
self.setNick(self._attemptedNick)
def irc_ERR_PASSWDMISMATCH(self, prefix, params):
ircLog(self.ident, "Password mismatch")
manager.handleWrongPassword(self.ident, prefix, params)
def irc_RPL_NAMREPLY(self, prefix, params):
channel = params[2]
nicklist = params[3].split(' ')
if channel not in self._names:
return
n = self._names[channel][1]
n += nicklist
def irc_RPL_ENDOFNAMES(self, prefix, params):
channel = params[1]
if channel not in self._names:
return
callbacks, namelist = self._names[channel]
for cb in callbacks:
cb.callback((channel, namelist))
del self._names[channel]
def got_names(self, nicklist):
ncklist = []
for i in nicklist[1]:
ncklist.append(self.sanitize(i))
self.nameslist[nicklist[0]] = ncklist
def who(self, channel):
channel = channel
d = defer.Deferred()
if channel not in self._who:
self._who[channel] = ([], {})
self._who[channel][0].append(d)
self.sendLine("WHO %s" % channel)
return d
def irc_RPL_WHOREPLY(self, prefix, params):
channel = params[1]
user = params[2]
host = params[3]
server = params[4]
nick = params[5]
status = params[6]
realname = params[7]
if channel not in self._who:
return
n = self._who[channel][1]
n[nick] = [nick, user, host, server, status, realname]
def irc_RPL_ENDOFWHO(self, prefix, params):
channel = params[1]
if channel not in self._who:
return
callbacks, info = self._who[channel]
for cb in callbacks:
cb.callback((channel, info))
del self._who[channel]
def got_who(self, whoinfo):
self.wholist[whoinfo[0]] = whoinfo[1]
def signedOn(self):
if self.authtype == "ns":
self.msg(self.authentity, "IDENTIFY %s" % self.nspass)
def joined(self, channel):
self.who(channel).addCallback(self.got_who)
class Base(Protocol):
def __init__(self, addr):
self.addr = addr
self.authed = False
if config["UsePassword"] == False:
self.authed = True
def send(self, data):
data += "\r\n"
data = data.encode("utf-8", "replace")
self.transport.write(data)
def dataReceived(self, data):
data = data.decode("utf-8", "replace")
log("Data received from %s:%s -- %s" % (self.addr.host, self.addr.port, repr(data)))
helper.parseCommand(self.addr, self.authed, data)
def connectionMade(self):
log("Connection from %s:%s" % (self.addr.host, self.addr.port))
self.send("Hello.")
def connectionLost(self, reason):
global connections
self.authed = False
log("Connection lost from %s:%s -- %s" % (self.addr.host, self.addr.port, reason.getErrorMessage()))
if not listener == None:
if self.addr in connections.keys():
del connections[self.addr]
else:
warn("Tried to remove a non-existant connection.")
else:
warn("Tried to remove a connection from a listener that wasn't running.")
class BaseFactory(Factory):
def buildProtocol(self, addr):
global connections
entry = Base(addr)
connections[addr] = entry
return entry
def send(self, addr, data):
global connections
if addr in connections.keys():
connection = connections[addr]
connection.send(data)
else:
return
class Helper(object):
def getConfig(self):
with open("config.json", "r") as f:
config = load(f)
if set(["Port", "BindAddress", "UseSSL", "UsePassword"]).issubset(set(config.keys())):
if config["UseSSL"] == True:
if not set(["ListenerKey", "ListenerCertificate"]).issubset(set(config.keys())):
error("SSL is on but certificate or key is not defined")
if config["UsePassword"] == True:
if not "Password" in config.keys():
error("Password authentication is on but password is not defined")
return config
else:
error("Mandatory values missing from config")
def getPool(self):
with open("pool.json", "r") as f:
data = f.read()
if not data == "":
pool = loads(data.strip())
return pool
else:
return {}
def savePool(self):
global pool
with open("pool.json", "w") as f:
dump(pool, f, indent=4)
return
def getHelp(self):
with open("help.json", "r") as f:
help = load(f)
return help
def incorrectUsage(self, addr, mode):
if mode == None:
sendFailure(addr, "Incorrect usage")
return
if mode in help.keys():
sendFailure(addr, "Usage: " + help[mode])
return
def addBot(self, name):
global IRCPool
instance = pool[name]
if instance["protocol"] == "plain":
if instance["bind"] == None:
point = TCP4ClientEndpoint(reactor, instance["host"], int(instance["port"]), timeout=int(instance["timeout"]))
bot = IRCBot(name)
IRCPool[name] = bot
d = connectProtocol(point, bot)
return
else:
point = TCP4ClientEndpoint(reactor, instance["host"], int(instance["port"]), timeout=int(instance["timeout"]), bindAddress=instance["bind"])
bot = IRCBot(name)
IRCPool[name] = bot
d = connectProtocol(point, bot)
return
elif instance["protocol"] == "ssl":
contextFactory = DefaultOpenSSLContextFactory(instance["key"].encode("utf-8", "replace"), instance["certificate"].encode("utf-8", "replace"))
if instance["bind"] == None:
point = SSL4ClientEndpoint(reactor, instance["host"], int(instance["port"]), contextFactory, timeout=int(instance["timeout"]))
bot = IRCBot(name)
IRCPool[name] = bot
d = connectProtocol(point, bot)
return
else:
point = SSL4ClientEndpoint(reactor, instance["host"], int(instance["port"]), contextFactory, timeout=int(instance["timeout"]), bindAddress=instance["bind"])
bot = IRCBot(name)
IRCPool[name] = bot
d = connectProtocol(point, bot)
return
def parseCommand(self, addr, authed, data):
global pool
data = data.strip()
spl = data.split()
obj = connections[addr]
success = lambda data: sendSuccess(addr, data)
failure = lambda data: sendFailure(addr, data)
info = lambda data: sendInfo(addr, data)
incUsage = lambda mode: self.incorrectUsage(addr, mode)
length = len(spl)
if len(spl) > 0:
cmd = spl[0]
else:
failure("No text was sent")
return
if authed == True:
if cmd == "help":
helpMap = []
for i in help.keys():
helpMap.append("%s: %s" % (i, help[i]))
info("\n".join(helpMap))
return
elif cmd == "rehash":
global config
config = helper.getConfig()
success("Configuration rehashed successfully")
elif cmd == "pass":
info("You are already authenticated")
return
elif cmd == "logout":
obj.authed = False
success("Logged out")
return
elif cmd == "list":
poolMap = []
for i in pool.keys():
poolMap.append("Server: %s" % i)
for x in pool[i].keys():
poolMap.append("%s: %s" % (x, pool[i][x]))
poolMap.append("\n")
info("\n".join(poolMap))
return
elif cmd == "enable":
if length == 2:
if not spl[1] in pool.keys():
failure("Name does not exist: %s" % spl[1])
return
pool[spl[1]]["enabled"] = True
self.addBot(spl[1])
success("Successfully enabled bot %s" % spl[1])
return
else:
incUsage("enable")
return
elif cmd == "disable":
if length == 2:
if not spl[1] in pool.keys():
failure("Name does not exist: %s" % spl[1])
return
pool[spl[1]]["enabled"] = False
if spl[1] in IRCPool.keys():
IRCPool[spl[1]].transport.loseConnection()
success("Successfully disabled bot %s" % spl[1])
return
else:
incUsage("disable")
return
elif cmd == "add":
if length == 6:
if spl[1] in pool.keys():
failure("Name already exists: %s" % spl[1])
return
protocol = spl[4].lower()
if not protocol in ["ssl", "plain"]:
incUsage("connect")
return
try:
int(spl[3])
except:
failure("Port must be an integer, not %s" % spl[3])
return
pool[spl[1]] = { "host": spl[2],
"port": spl[3],
"protocol": protocol,
"bind": None,
"timeout": 30,
"nickname": spl[5],
"username": None,
"realname": None,
"userinfo": None,
"finger": None,
"version": None,
"source": None,
"authtype": None,
"password": None,
"authentity": "NickServ",
"key": config["ListenerKey"],
"certificate": config["ListenerCertificate"],
"enabled": config["ConnectOnCreate"],
}
if config["ConnectOnCreate"] == True:
self.addBot(spl[1])
success("Successfully created bot")
self.savePool()
return
else:
incUsage("add")
return
elif cmd == "del":
if length == 2:
if not spl[1] in pool.keys():
failure("Name does not exist: %s" % spl[1])
return
del pool[spl[1]]
if spl[1] in IRCPool.keys():
IRCPool[spl[1]].transport.loseConnection()
del IRCPool[spl[1]]
success("Successfully removed bot")
self.savePool()
return
else:
incUsage("del")
return
elif cmd == "mod":
toUnset = False
if length == 2:
if not spl[1] in pool.keys():
failure("Name does not exist: %s" % spl[1])
return
optionMap = ["Viewing options for %s" % spl[1]]
for i in pool[spl[1]].keys():
optionMap.append("%s: %s" % (i, pool[spl[1]][i]))
info("\n".join(optionMap))
return
elif length == 3:
if not spl[1] in pool.keys():
failure("Name does not exist: %s" % spl[1])
return
if not spl[2] in pool[spl[1]].keys():
failure("No such key: %s" % spl[2])
return
info("%s: %s" % (spl[2], pool[spl[1]][spl[2]]))
return
elif length == 4:
if not spl[1] in pool.keys():
failure("Name does not exist: %s" % spl[1])
return
if not spl[2] in pool[spl[1]].keys():
failure("No such key: %s" % spl[2])
return
if spl[2] in ["port", "timeout"]:
try:
int(spl[3])
except:
failure("Value must be an integer, not %s" % spl[3])
return
if spl[2] == "protocol":
if not spl[3] in ["ssl", "plain"]:
failure("Protocol must be ssl or plain, not %s" % spl[3])
return
if spl[3] == pool[spl[1]][spl[2]]:
failure("Value already exists: %s" % spl[3])
return
if spl[3].lower() in ["none", "nil"]:
spl[3] = None
toUnset = True
if spl[2] == "authtype":
if not spl[3] in ["sp", "ns"]:
failure("Authtype must be sp or ns, not %s" % spl[3])
return
if spl[2] == "enabled":
failure("Use the enable and disable commands to manage this")
return
pool[spl[1]][spl[2]] = spl[3]
self.savePool()
if toUnset:
success("Successfully unset key %s on %s" % (spl[2], spl[3], spl[1]))
else:
success("Successfully set key %s to %s on %s" % (spl[2], spl[3], spl[1]))
return
else:
incUsage("mod")
else:
incUsage(None)
return
else:
if cmd == "pass" and length == 2:
if spl[1] == config["Password"]:
success("Authenticated successfully")
obj.authed = True
return
else:
failure("Password incorrect")
obj.transport.loseConnection()
return
else:
incUsage(None)
return
if __name__ == "__main__":
helper = Helper()
config = helper.getConfig()
pool = helper.getPool()
help = helper.getHelp()
for i in pool.keys():
if pool[i]["enabled"] == True:
helper.addBot(i)
listener = BaseFactory()
if config["UseSSL"] == True:
reactor.listenSSL(config["Port"], listener, DefaultOpenSSLContextFactory(config["ListenerKey"], config["ListenerCertificate"]), interface=config["BindAddress"])
log("Threshold running with SSL on %s:%s" % (config["BindAddress"], config["Port"]))
else:
reactor.listenTCP(config["Port"], listener, interface=config["BindAddress"])
log("Threshold running on %s:%s" % (config["BindAddress"], config["Port"]))
reactor.run()