diff --git a/.gitignore b/.gitignore index 23ae7fa..76cbb56 100644 --- a/.gitignore +++ b/.gitignore @@ -10,5 +10,6 @@ conf/masterbuf.json conf/monitor.json conf/alias.json conf/relay.json +conf/network.json conf/dist.sh env/ diff --git a/commands/alias.py b/commands/alias.py index a466de8..8baa85b 100644 --- a/commands/alias.py +++ b/commands/alias.py @@ -7,15 +7,17 @@ class Alias: def alias(self, addr, authed, data, obj, spl, success, failure, info, incUsage, length): if authed: - if length == 6: + if length == 8: if spl[1] == "add": if spl[2] in main.alias.keys(): failure("Alias already exists: %s" % spl[2]) return else: main.alias[spl[2]] = {"nick": spl[3], - "ident": spl[4], - "realname": spl[5]} + "altnick": spl[4], + "ident": spl[5], + "realname": spl[6], + "password": spl[7]} success("Successfully created alias: %s" % spl[2]) main.saveConf("alias") return diff --git a/commands/default.py b/commands/default.py deleted file mode 100644 index d432cf1..0000000 --- a/commands/default.py +++ /dev/null @@ -1,80 +0,0 @@ -import main - -class Default: - def __init__(self, register): - register("default", self.default) - - def default(self, addr, authed, data, obj, spl, success, failure, info, incUsage, length): - if authed: - toUnset = False - if length == 1: - optionMap = ["Viewing defaults"] - for i in main.config["Default"].keys(): - optionMap.append("%s: %s" % (i, main.config["Default"][i])) - info("\n".join(optionMap)) - return - elif length == 2: - if not spl[1] in main.config["Default"].keys(): - failure("No such key: %s" % spl[1]) - return - info("%s: %s" % (spl[1], main.config["Default"][spl[1]])) - return - elif length == 3: - if not spl[1] in main.config["Default"].keys(): - failure("No such key: %s" % spl[1]) - return - - if spl[2].lower() in ["none", "nil"]: - spl[2] = None - toUnset = True - - if spl[1] in ["port", "timeout", "maxdelay"]: - try: - spl[2] = int(spl[2]) - except: - failure("Value must be an integer, not %s" % spl[2]) - return - - if spl[2] in ["initialdelay", "factor", "jitter"]: - try: - spl[3] = float(spl[3]) - except: - failure("Value must be a floating point integer, not %s" % spl[3]) - return - - if spl[1] == "protocol": - if not toUnset: - if not spl[2] in ["ssl", "plain"]: - failure("Protocol must be ssl or plain, not %s" % spl[2]) - return - - if spl[2] == main.config["Default"][spl[1]]: - failure("Value already exists: %s" % spl[2]) - return - - if spl[1] == "authtype": - if not toUnset: - if not spl[2] in ["sp", "ns"]: - failure("Authtype must be sp or ns, not %s" % spl[2]) - return - if spl[1] == "enabled": - failure("Use the ConnectOnCreate main.config parameter to set this") - return - if spl[1] == "autojoin": - if not toUnset: - spl[2] = spl[2].split(",") - else: - spl[2] = [] - - main.config["Default"][spl[1]] = spl[2] - main.saveConf("config") - if toUnset: - success("Successfully unset key %s" % spl[1]) - else: - success("Successfully set key %s to %s" % (spl[1], spl[2])) - return - else: - incUsage("default") - return - else: - incUsage(None) diff --git a/commands/network.py b/commands/network.py new file mode 100644 index 0000000..e78d87d --- /dev/null +++ b/commands/network.py @@ -0,0 +1,60 @@ +import main +from yaml import dump + +class Network: + def __init__(self, register): + register("network", self.network) + + def network(self, addr, authed, data, obj, spl, success, failure, info, incUsage, length): + if authed: + if length == 7: + if spl[1] == "add": + if spl[2] in main.network.keys(): + failure("Network already exists: %s" % spl[2]) + return + if not spl[4].isdigit(): + failure("Port must be an integer, not %s" % spl[4]) + return + if not spl[5].lower() in ["ssl", "plain"]: + failure("Security must be ssl or plain, not %s" % spl[5]) + return + if not spl[6].lower() in ["sasl", "ns", "none"]: + failure("Auth must be sasl, ns or none, not %s" % spl[5]) + return + else: + main.network[spl[2]] = {"host": spl[3], + "port": spl[4], + "security": spl[5].lower(), + "auth": spl[6].lower()} + success("Successfully created network: %s" % spl[2]) + main.saveConf("network") + return + else: + incUsage("network") + return + + elif length == 3: + if spl[1] == "del": + if spl[2] in main.network.keys(): + del main.network[spl[2]] + success("Successfully removed network: %s" % spl[2]) + main.saveConf("network") + return + else: + failure("No such network: %s" % spl[2]) + return + else: + incUsage("network") + return + elif length == 2: + if spl[1] == "list": + info(dump(main.network)) + return + else: + incUsage("network") + return + else: + incUsage("network") + return + else: + incUsage(None) diff --git a/commands/provision.py b/commands/provision.py new file mode 100644 index 0000000..e1c2556 --- /dev/null +++ b/commands/provision.py @@ -0,0 +1,59 @@ +import main +from modules import provision + +class Provision: + def __init__(self, register): + register("provision", self.provision) + + def provision(self, addr, authed, data, obj, spl, success, failure, info, incUsage, length): + if authed: + if length == 4 or length == 3: + if not spl[1] in main.relay.keys(): + failure("No such relay: %s" % spl[1]) + return + if not spl[2] in main.alias.keys(): + failure("No such alias: %s" % spl[2]) + return + if length == 4: # provision for relay, alias and network + if not spl[3] in main.network.keys(): + failure("No such network: %s" % spl[3]) + return + + if "users" in main.relay[spl[1]]: + if not spl[2] in main.relay[spl[1]]["users"]: + failure("Relay %s not provisioned for alias %s" % (spl[1], spl[2])) + return + else: + failure("Relay %s not provisioned for alias %s" % (spl[1], spl[2])) + return + + rtrn = provision.provisionRelayForNetwork(spl[1], spl[2], spl[3]) + if rtrn == "PROVISIONED": + failure("Relay %s already provisioned for network %s" % (spl[1], spl[3])) + return + elif rtrn == "DUPLICATE": + failure("Instance with relay %s and alias %s already exists for network %s" % (spl[1], spl[2], spl[3])) + elif rtrn: + success("Started provisioning network %s on relay %s for alias %s" % (spl[3], spl[1], spl[2])) + info("Instance name is %s" % rtrn) + return + else: + failure("Failure while provisioning relay %s" % spl[1]) + return + if length == 3: # provision for relay and alias only + rtrn = provision.provisionRelayForAlias(spl[1], spl[2]) + if rtrn == "PROVISIONED": + failure("Relay %s already provisioned for alias %s" % (spl[1], spl[2])) + return + elif rtrn: + success("Started provisioning relay %s for alias %s" % (spl[1], spl[2])) + return + else: + failure("Failure while provisioning relay %s" % spl[1]) + return + + else: + incUsage("provision") + return + else: + incUsage(None) diff --git a/commands/relay.py b/commands/relay.py index d611fc2..0ad2bf4 100644 --- a/commands/relay.py +++ b/commands/relay.py @@ -12,10 +12,10 @@ class Relay: if spl[2] in main.relay.keys(): failure("Relay already exists: %s" % spl[2]) return + if not spl[4].isdigit(): + failure("Port must be an integer, not %s" % spl[4]) + return else: - if not spl[4].isdigit(): - failure("Port must be an integer, not %s" % spl[4]) - return main.relay[spl[2]] = {"host": spl[3], "port": spl[4], "user": spl[5], diff --git a/conf/example/config.json b/conf/example/config.json index 93a7527..2b2959d 100644 --- a/conf/example/config.json +++ b/conf/example/config.json @@ -2,13 +2,14 @@ "Listener": { "Port": 13867, "Address": "127.0.0.1", - "UseSSL": true, - "Key": "key.pem", - "Certificate": "cert.pem" + "UseSSL": true }, + "Key": "key.pem", + "Certificate": "cert.pem", "RedisSocket": "/tmp/redis.sock", "UsePassword": true, "ConnectOnCreate": false, + "RelayPassword": "s", "Notifications": { "Highlight": true, "Connection": true, @@ -29,32 +30,13 @@ }, "Delays": { "WhoLoop": 600, - "WhoRange": 1800 + "WhoRange": 1800, + "Timeout": 30, + "MaxDelay": 360, + "InitialDelay": 1.0, + "Factor": 2.718281828459045, + "Jitter": 0.11962656472 } }, - "Default": { - "host": null, - "port": null, - "protocol": "ssl", - "bind": null, - "timeout": 30, - "maxdelay": 360, - "initialdelay": 1.0, - "factor": 2.7182818284590451, - "jitter": 0.11962656472, - "nickname": null, - "username": null, - "realname": null, - "userinfo": null, - "finger": null, - "version": null, - "source": null, - "autojoin": [], - "authtype": null, - "password": null, - "authentity": "NickServ", - "key": "key.pem", - "certificate": "cert.pem" - }, "Master": [null, null] } diff --git a/conf/example/network.json b/conf/example/network.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/conf/example/network.json @@ -0,0 +1 @@ +{} diff --git a/conf/help.json b/conf/help.json index d4d8c7b..0c77572 100644 --- a/conf/help.json +++ b/conf/help.json @@ -1,10 +1,8 @@ { "pass": "pass ", "logout": "logout", - "add": "add [
] [] [] []", "del": "del ", "mod": "mod [] []", - "default": "default [] []", "get": "get ", "key": "key [] [] [] []", "who": "who ", @@ -22,6 +20,8 @@ "mon": "mon -h", "chans": "chans [ ...]", "users": "users [ ...]", - "alias": "alias [ ]", - "relay": "relay [ " + "alias": "alias [ ]", + "relay": "relay [ ]", + "network": "network [
]", + "provision": "provision []" } diff --git a/core/bot.py b/core/bot.py index b001818..3f063c6 100644 --- a/core/bot.py +++ b/core/bot.py @@ -1,7 +1,8 @@ from twisted.internet.protocol import ReconnectingClientFactory from twisted.words.protocols.irc import IRCClient from twisted.internet.defer import Deferred -from twisted.internet.task import LoopingCall +from twisted.internet.task import LoopingCall, deferLater +from twisted.internet import reactor from string import digits from random import randint @@ -15,6 +16,76 @@ import main from utils.logging.log import * from utils.logging.send import * +from twisted.internet.ssl import DefaultOpenSSLContextFactory + +def deliverRelayCommands(relay, relayCommands, user=None, stage2=None): + keyFN = main.certPath+main.config["Key"] + certFN = main.certPath+main.config["Certificate"] + contextFactory = DefaultOpenSSLContextFactory(keyFN.encode("utf-8", "replace"), + certFN.encode("utf-8", "replace")) + bot = IRCBotFactory(None, relay, relayCommands, user, stage2) + rct = reactor.connectSSL(main.relay[relay]["host"], + int(main.relay[relay]["port"]), + bot, contextFactory) + +class IRCRelay(IRCClient): + def __init__(self, relay, relayCommands, user, stage2): + self.connected = False + self.buffer = "" + if user == None: + self.user = main.relay[relay]["user"] + else: + self.user = user + password = main.relay[relay]["password"] + self.nickname = self.user + self.realname = self.user + self.username = self.user + self.password = self.user+":"+password + + self.relayCommands = relayCommands + self.relay = relay + self.stage2 = stage2 + + def parsen(self, user): + step = user.split("!") + nick = step[0] + if len(step) == 2: + step2 = step[1].split("@") + ident, host = step2 + else: + ident = nick + host = nick + + return [nick, ident, host] + + def privmsg(self, user, channel, msg): + nick, ident, host = self.parsen(user) + if nick[0] == main.config["Tweaks"]["ZNC"]["Prefix"]: + nick = nick[1:] + if nick in self.relayCommands.keys(): + sendAll("[%s] %s -> %s" % (self.relay, nick, msg)) + + def irc_ERR_PASSWDMISMATCH(self, prefix, params): + log("%s: relay password mismatch" % self.relay) + sendAll("%s: relay password mismatch" % self.relay) + + def signedOn(self): + self.connected = True + log("signed on as a relay: %s" % self.relay) + if main.config["Notifications"]["Connection"]: + keyword.sendMaster("SIGNON: %s" % self.relay) + for i in self.relayCommands.keys(): + for x in self.relayCommands[i]: + self.msg(main.config["Tweaks"]["ZNC"]["Prefix"]+i, x) + if not self.stage2 == None: # [["user", {"sasl": ["message1", "message2"]}], []] + if not len(self.stage2) == 0: + user = self.stage2[0].pop(0) + commands = self.stage2[0].pop(0) + del self.stage2[0] + deliverRelayCommands(self.relay, commands, user, self.stage2) + deferLater(reactor, 1, self.transport.loseConnection) + return + class IRCBot(IRCClient): def __init__(self, name): self.connected = False @@ -380,33 +451,46 @@ class IRCBot(IRCClient): monitor.event(self.net, channel, {"type": "mode", "exact": user, "nick": nick, "ident": ident, "host": host, "modes": m, "modeargs": a}) class IRCBotFactory(ReconnectingClientFactory): - def __init__(self, name): - self.instance = main.pool[name] - self.name = name - self.net = "".join([x for x in self.name if not x in digits]) + def __init__(self, name, relay=None, relayCommands=None, user=None, stage2=None): + if not name == None: + self.name = name + self.instance = main.pool[name] + self.net = "".join([x for x in self.name if not x in digits]) + else: + self.name = "Relay to "+relay self.client = None - self.maxDelay = self.instance["maxdelay"] - self.initialDelay = self.instance["initialdelay"] - self.factor = self.instance["factor"] - self.jitter = self.instance["jitter"] + self.maxDelay = main.config["Tweaks"]["Delays"]["MaxDelay"] + self.initialDelay = main.config["Tweaks"]["Delays"]["InitialDelay"] + self.factor = main.config["Tweaks"]["Delays"]["Factor"] + self.jitter = main.config["Tweaks"]["Delays"]["Jitter"] + + self.relay, self.relayCommands, self.user, self.stage2 = relay, relayCommands, user, stage2 def buildProtocol(self, addr): - entry = IRCBot(self.name) - main.IRCPool[self.name] = entry + + if self.relay == None: + entry = IRCBot(self.name) + main.IRCPool[self.name] = entry + else: + entry = IRCRelay(self.relay, self.relayCommands, self.user, self.stage2) + self.client = entry return entry def clientConnectionLost(self, connector, reason): - userinfo.delNetwork(self.net, self.client.channels) + if not self.relay: + userinfo.delNetwork(self.net, self.client.channels) if not self.client == None: self.client.connected = False self.client.channels = [] error = reason.getErrorMessage() log("%s: connection lost: %s" % (self.name, error)) - sendAll("%s: connection lost: %s" % (self.name, error)) + if not self.relay: + sendAll("%s: connection lost: %s" % (self.name, error)) if main.config["Notifications"]["Connection"]: keyword.sendMaster("CONNLOST %s: %s" % (self.name, error)) - self.retry(connector) + if not self.relay: + self.retry(connector) #ReconnectingClientFactory.clientConnectionLost(self, connector, reason) def clientConnectionFailed(self, connector, reason): @@ -418,6 +502,7 @@ class IRCBotFactory(ReconnectingClientFactory): sendAll("%s: connection failed: %s" % (self.name, error)) if main.config["Notifications"]["Connection"]: keyword.sendMaster("CONNFAIL %s: %s" % (self.name, error)) - self.retry(connector) + if not self.relay: + self.retry(connector) #ReconnectingClientFactory.clientConnectionFailed(self, connector, reason) diff --git a/core/helper.py b/core/helper.py index 98e6364..570fd2c 100644 --- a/core/helper.py +++ b/core/helper.py @@ -1,35 +1,27 @@ from twisted.internet import reactor -from twisted.internet.ssl import DefaultOpenSSLContextFactory - from core.bot import IRCBot, IRCBotFactory import main from utils.logging.log import * def addBot(name): instance = main.pool[name] - log("Started bot %s to %s:%s protocol %s nickname %s" % (name, instance["host"], instance["port"], instance["protocol"], instance["nickname"])) - if instance["protocol"] == "plain": - if instance["bind"] == None: - bot = IRCBotFactory(name) - rct = reactor.connectTCP(instance["host"], instance["port"], bot, timeout=int(instance["timeout"])) + log("Started bot %s to %s:%s protocol %s nickname %s" % (name, + instance["host"], + instance["port"], + instance["protocol"], + instance["nickname"])) - main.ReactorPool[name] = rct - main.FactoryPool[name] = bot - return - else: - bot = IRCBotFactory(name) - rct = reactor.connectTCP(instance["host"], instance["port"], bot, timeout=int(instance["timeout"]), bindAddress=instance["bind"]) - main.ReactorPool[name] = rct - main.FactoryPool[name] = bot - return - elif instance["protocol"] == "ssl": - keyFN = main.certPath+instance["key"] - certFN = main.certPath+instance["certificate"] - contextFactory = DefaultOpenSSLContextFactory(keyFN.encode("utf-8", "replace"), certFN.encode("utf-8", "replace")) + if instance["protocol"] == "ssl": + keyFN = main.certPath+main.config["Key"] + certFN = main.certPath+main.config["Certificate"] + contextFactory = DefaultOpenSSLContextFactory(keyFN.encode("utf-8", "replace"), + certFN.encode("utf-8", "replace")) if instance["bind"] == None: bot = IRCBotFactory(name) - rct = reactor.connectSSL(instance["host"], int(instance["port"]), bot, contextFactory) + rct = reactor.connectSSL(instance["host"], + int(instance["port"]), + bot, contextFactory) main.ReactorPool[name] = rct main.FactoryPool[name] = bot @@ -37,7 +29,10 @@ def addBot(name): else: bot = IRCBotFactory(name) - rct = reactor.connectSSL(instance["host"], int(instance["port"]), bot, contextFactory, bindAddress=instance["bind"]) + rct = reactor.connectSSL(instance["host"], + int(instance["port"]), + bot, contextFactory, + bindAddress=instance["bind"]) main.ReactorPool[name] = rct main.FactoryPool[name] = bot diff --git a/main.py b/main.py index aa54dca..75fe1e7 100644 --- a/main.py +++ b/main.py @@ -10,13 +10,14 @@ certPath = "cert/" filemap = { "config": ["config.json", "configuration"], "keyconf": ["keyword.json", "keyword lists"], - "pool": ["pool.json", "pool"], + "pool": ["pool.json", "network, alias and relay mappings"], "help": ["help.json", "command help"], "counters": ["counters.json", "counters file"], "masterbuf": ["masterbuf.json", "master buffer"], "monitor": ["monitor.json", "monitoring database"], "alias": ["alias.json", "alias details"], "relay": ["relay.json", "relay list"], + "network": ["network.json", "network list"], } connections = {} diff --git a/modules/provision.py b/modules/provision.py new file mode 100644 index 0000000..8d5f96a --- /dev/null +++ b/modules/provision.py @@ -0,0 +1,87 @@ +import main +from core.bot import deliverRelayCommands +from utils.logging.log import * + +def provisionUserData(relay, alias, nick, altnick, ident, realname, password): + commands = {} + commands["controlpanel"] = [] + commands["controlpanel"].append("AddUser %s %s" % (alias, password)) + commands["controlpanel"].append("Set Nick %s %s" % (alias, nick)) + commands["controlpanel"].append("Set Altnick %s %s" % (alias, altnick)) + commands["controlpanel"].append("Set Ident %s %s" % (alias, ident)) + commands["controlpanel"].append("Set RealName %s %s" % (alias, realname)) + deliverRelayCommands(relay, commands) + return + +def provisionNetworkData(relay, alias, network, host, port, security, auth, password): + commands = {} + stage2commands = {} + commands["controlpanel"] = [] + commands["controlpanel"].append("AddNetwork %s %s" % (alias, network)) + if security == "ssl": + commands["controlpanel"].append("SetNetwork TrustAllCerts %s %s true" % (alias, network)) # Don't judge me + commands["controlpanel"].append("AddServer %s %s %s +%s" % (alias, network, host, port)) + elif security == "plain": + commands["controlpanel"].append("AddServer %s %s %s %s" % (alias, network, host, port)) + if auth == "sasl": + stage2commands["status"] = [] + stage2commands["sasl"] = [] + stage2commands["status"].append("LoadMod sasl") + stage2commands["sasl"].append("Mechanism plain") + stage2commands["sasl"].append("Set %s %s" % (alias, password)) + elif auth == "ns": + stage2commands["status"] = [] + stage2commands["nickserv"] = [] + stage2commands["status"].append("LoadMod NickServ") + stage2commands["nickserv"].append("Set %s" % password) + deliverRelayCommands(relay, commands, stage2=[[alias+"/"+network, stage2commands]]) + return + +def provisionRelayForAlias(relay, alias): + if "users" in main.relay[relay].keys(): + if alias in main.relay[relay]["users"]: + return "PROVISIONED" + else: + main.relay[relay]["users"] = [] + main.relay[relay]["users"].append(alias) + provisionUserData(relay, alias, main.alias[alias]["nick"], + main.alias[alias]["altnick"], + main.alias[alias]["ident"], + main.alias[alias]["realname"], + main.relay[relay]["password"]) + main.saveConf("relay") + return True + +def provisionRelayForNetwork(relay, alias, network): + if "networks" in main.relay[relay].keys(): + if network in main.relay[relay]["networks"]: + return "PROVISIONED" + else: + main.relay[relay]["networks"] = [] + main.relay[relay]["networks"].append(network) + provisionNetworkData(relay, alias, network, + main.network[network]["host"], + main.network[network]["port"], + main.network[network]["security"], + main.network[network]["auth"], + main.alias[alias]["password"]) + main.saveConf("relay") + storedNetwork = False + num = 1 + while not storedNetwork: + i = str(num) + if num == 1000: + error("Too many iterations in while trying to choose name for r: %s a: %s n: %s" % (relay, alias, network)) + return False + + if network+i in main.pool.keys(): + if main.pool[network+i]["alias"] == alias and main.pool[network+i]["relay"] == relay: + return "DUPLICATE" + num += 1 + else: + main.pool[network+i] = {"relay": relay, + "alias": alias, + "network": network} + main.saveConf("pool") + storedNetwork = True + return network+i diff --git a/threshold b/threshold index 71872a5..adab5cf 100755 --- a/threshold +++ b/threshold @@ -18,12 +18,14 @@ from core.server import Server, ServerFactory if __name__ == "__main__": listener = ServerFactory() if main.config["Listener"]["UseSSL"] == True: - reactor.listenSSL(main.config["Listener"]["Port"], listener, DefaultOpenSSLContextFactory(main.certPath+main.config["Listener"]["Key"], main.certPath+main.config["Listener"]["Certificate"]), interface=main.config["Listener"]["Address"]) + reactor.listenSSL(main.config["Listener"]["Port"], listener, DefaultOpenSSLContextFactory(main.certPath+main.config["Key"], main.certPath+main.config["Certificate"]), interface=main.config["Listener"]["Address"]) log("Threshold running with SSL on %s:%s" % (main.config["Listener"]["Address"], main.config["Listener"]["Port"])) else: reactor.listenTCP(main.config["Listener"]["Port"], listener, interface=main.config["Listener"]["Address"]) log("Threshold running on %s:%s" % (main.config["Listener"]["Address"], main.config["Listener"]["Port"])) for i in main.pool.keys(): + if not "enabled" in main.pool[i]: + continue if main.pool[i]["enabled"] == True: helper.addBot(i) diff --git a/utils/logging/send.py b/utils/logging/send.py index 50b858a..0f4eeee 100644 --- a/utils/logging/send.py +++ b/utils/logging/send.py @@ -20,7 +20,8 @@ def sendInfo(addr, data): def sendAll(data): for i in main.connections: - main.connections[i].send(data) + if main.connections[i].authed: + main.connections[i].send(data) return def incorrectUsage(addr, mode):