Implement the backend for automatically provisioning relays

This commit is contained in:
Mark Veidemanis 2019-01-26 01:57:24 +00:00
parent 6046329a83
commit 4efea3f535
15 changed files with 356 additions and 160 deletions

1
.gitignore vendored
View File

@ -10,5 +10,6 @@ conf/masterbuf.json
conf/monitor.json
conf/alias.json
conf/relay.json
conf/network.json
conf/dist.sh
env/

View File

@ -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

View File

@ -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)

60
commands/network.py Normal file
View File

@ -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)

59
commands/provision.py Normal file
View File

@ -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)

View File

@ -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],

View File

@ -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]
}

View File

@ -0,0 +1 @@
{}

View File

@ -1,10 +1,8 @@
{
"pass": "pass <password>",
"logout": "logout",
"add": "add <name> [<address>] [<port>] [<ssl|plain>] [<nickname>]",
"del": "del <name>",
"mod": "mod <name> [<key>] [<value>]",
"default": "default [<key>] [<value>]",
"get": "get <name> <variable>",
"key": "key <master|list|add|del|except|unexcept|listexcept|monitor> [<name>] [<target>] [<key...>] [<on|off>]",
"who": "who <query>",
@ -22,6 +20,8 @@
"mon": "mon -h",
"chans": "chans <nick> [<nick> ...]",
"users": "users <channel> [<channel> ...]",
"alias": "alias <add|del|list> [<alias> <nickname> <ident> <realname>]",
"relay": "relay <add|del|list> [<relay> <host> <port> <user> <password>"
"alias": "alias <add|del|list> [<alias> <nickname> <altnick> <ident> <realname> <password>]",
"relay": "relay <add|del|list> [<relay> <host> <port> <user> <password>]",
"network": "network <add|del|list> [<name> <address> <port> <ssl|plain> <sasl|ns|none>]",
"provision": "provision <relay> <alias> [<network>]"
}

View File

@ -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)

View File

@ -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":
log("Started bot %s to %s:%s protocol %s nickname %s" % (name,
instance["host"],
instance["port"],
instance["protocol"],
instance["nickname"]))
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.connectTCP(instance["host"], instance["port"], bot, timeout=int(instance["timeout"]))
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["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

View File

@ -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 = {}

87
modules/provision.py Normal file
View File

@ -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

View File

@ -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)

View File

@ -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):