|
@@ -5,6 +5,7 @@ import willie
|
|
|
import datetime
|
|
|
import difflib
|
|
|
from email.utils import mktime_tz
|
|
|
+from fnmatch import fnmatch
|
|
|
import git
|
|
|
import netaddr
|
|
|
import json
|
|
@@ -26,6 +27,8 @@ peers_repo = None
|
|
|
monitored_nodes = None
|
|
|
highscores = None
|
|
|
|
|
|
+nodeaccess = None
|
|
|
+
|
|
|
alfred_method = None
|
|
|
alfred_data = None
|
|
|
alfred_update = datetime.datetime(1970,1,1,23,42)
|
|
@@ -74,7 +77,7 @@ class ThreadingTCPServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer):
|
|
|
pass
|
|
|
|
|
|
def setup(bot):
|
|
|
- global msgserver, peers_repo, alfred_method, highscores, monitored_nodes
|
|
|
+ global msgserver, peers_repo, alfred_method, highscores, monitored_nodes, nodeaccess
|
|
|
|
|
|
# signal begin of setup routine
|
|
|
bot.memory['ffpb_in_setup'] = True
|
|
@@ -95,6 +98,9 @@ def setup(bot):
|
|
|
seen_nodes = shelve.open('nodes.seen', writeback=True)
|
|
|
bot.memory['seen_nodes'] = seen_nodes
|
|
|
|
|
|
+ # load list of node ACL from disk (used in playitsafe())
|
|
|
+ nodeaccess = shelve.open('nodes.acl', writeback=True)
|
|
|
+
|
|
|
# no need to configure anything else if the ffpb config section is missing
|
|
|
if not bot.config.has_section('ffpb'):
|
|
|
bot.memory['ffpb_in_setup'] = False
|
|
@@ -129,7 +135,7 @@ def setup(bot):
|
|
|
bot.memory['ffpb_in_setup'] = False
|
|
|
|
|
|
def shutdown(bot):
|
|
|
- global msgserver, highscores, monitored_nodes
|
|
|
+ global msgserver, highscores, monitored_nodes, nodeaccess
|
|
|
|
|
|
# store highscores
|
|
|
if not highscores is None:
|
|
@@ -143,6 +149,12 @@ def shutdown(bot):
|
|
|
monitored_nodes.close()
|
|
|
monitored_nodes = None
|
|
|
|
|
|
+ # store node acl
|
|
|
+ if not nodeaccess is None:
|
|
|
+ nodeaccess.sync()
|
|
|
+ nodeaccess.close()
|
|
|
+ nodeaccess = None
|
|
|
+
|
|
|
# store seen nodes
|
|
|
if 'seen_nodes' in bot.memory and bot.memory['seen_nodes'] != None:
|
|
|
bot.memory['seen_nodes'].close()
|
|
@@ -184,6 +196,145 @@ def ffpb_help(bot, trigger):
|
|
|
|
|
|
bot.say("Allgemeine Hilfe gibt's mit !help - ohne Parameter.")
|
|
|
|
|
|
+def playitsafe(bot, trigger,
|
|
|
+ botadmin=False, admin_channel=False, via_channel=False, via_privmsg=False, need_op=False, node=None,
|
|
|
+ reply_directly=True, debug_user=None, debug_ignorebotadmin=False):
|
|
|
+ """helper: checks that the triggering user has the necessary rights
|
|
|
+
|
|
|
+ Returns true if everything is okay. If it's not, a reply is send via the bot and false is returned.
|
|
|
+ """
|
|
|
+
|
|
|
+ if via_channel and via_privmsg:
|
|
|
+ raise Exception('Der Entwickler ist ein dummer, dummer Junge (playitsafe hat via_channel und via_privmsg gleichzeitig gesetzt).')
|
|
|
+
|
|
|
+ user = trigger.nick if debug_user is None else debug_user
|
|
|
+ user = user.lower()
|
|
|
+
|
|
|
+ # botadmin: you need to be configured as a bot admin
|
|
|
+ if botadmin and not trigger.admin:
|
|
|
+ if reply_directly: bot.say('Du brauchst Super-Kuh-Kräfte um dieses Kommando auszuführen.')
|
|
|
+ return False
|
|
|
+
|
|
|
+ # via_channel: the request must not be a private conversation
|
|
|
+ if via_channel and trigger.is_privmsg:
|
|
|
+ if reply_directly: bot.say('Bitte per Channel - mehr Transparenz wagen und so!')
|
|
|
+ return False
|
|
|
+
|
|
|
+ # via_privmsg: the request must be a private conversation
|
|
|
+ if via_privmsg and not trigger.is_privmsg:
|
|
|
+ if reply_directly: bot.say('Solche Informationen gibt es nur per PM, da bin ich ja schon ein klein wenig sensibel ...')
|
|
|
+ return False
|
|
|
+
|
|
|
+ # need_op: if the message is in a channel, check that the user has OP there
|
|
|
+ if need_op and (not trigger.is_privmsg) and (not user in bot.ops[trigger.sender]):
|
|
|
+ if reply_directly: bot.say('Keine Zimtschnecke, keine Kekse.')
|
|
|
+ return False
|
|
|
+
|
|
|
+ # node: check that the user is whitelisted (or is admin)
|
|
|
+ if not node is None and (debug_ignorebotadmin or not trigger.admin):
|
|
|
+ acluser = [ x for x in nodeaccess if x.lower() == user ]
|
|
|
+ acluser = acluser[0] if len(acluser) == 1 else None
|
|
|
+ if nodeaccess is None or acluser is None:
|
|
|
+ if reply_directly: bot.reply('You! Shall! Not! Access!')
|
|
|
+ return False
|
|
|
+
|
|
|
+ nodeid = node['node_id'] if 'node_id' in node else None
|
|
|
+ matched = False
|
|
|
+ for x in nodeaccess[acluser]:
|
|
|
+ if x == nodeid or fnmatch(node['hostname'], x):
|
|
|
+ matched = True
|
|
|
+ break
|
|
|
+
|
|
|
+ if not matched:
|
|
|
+ if reply_directly: bot.reply('Mach das doch bitte auf deinen Knoten, kthxbye.')
|
|
|
+ return False
|
|
|
+
|
|
|
+ return True
|
|
|
+
|
|
|
+@willie.module.commands('nodeacl')
|
|
|
+def ffpb_nodeacl(bot, trigger):
|
|
|
+ """Configure ACL for nodes."""
|
|
|
+
|
|
|
+ if not playitsafe(bot, trigger, botadmin=True):
|
|
|
+ # the check function already gives a bot reply, just exit here
|
|
|
+ return
|
|
|
+
|
|
|
+ # ensure the user gave arguments (group 2 is the concatenation of all following groups)
|
|
|
+ if trigger.group(2) is None or len(trigger.group(2)) == 0:
|
|
|
+ bot.say('Sag doch was du willst ... einmal mit Profis arbeiten, ey -.-')
|
|
|
+ return
|
|
|
+
|
|
|
+ # read additional arguments
|
|
|
+ cmd = trigger.group(3).lower()
|
|
|
+
|
|
|
+ if cmd == 'list':
|
|
|
+ user = trigger.group(4)
|
|
|
+ if user is None:
|
|
|
+ bot.say('ACLs gesetzt für die User: ' + ', '.join([x for x in nodeaccess]))
|
|
|
+ return
|
|
|
+
|
|
|
+ user = user.lower()
|
|
|
+ uid = [ x for x in nodeaccess if x.lower() == user ]
|
|
|
+ if len(uid) == 0:
|
|
|
+ bot.say('Für \'{0}\' ist keine Node ACL gesetzt.'.format(user))
|
|
|
+ return
|
|
|
+
|
|
|
+ bot.say('Node ACL für \'{0}\' = \'{1}\''.format(uid[0], '\', \''.join(nodeaccess[uid[0]])))
|
|
|
+ return
|
|
|
+
|
|
|
+ if cmd in [ 'add', 'del', 'check' ]:
|
|
|
+ user = trigger.group(4)
|
|
|
+ value = trigger.group(5)
|
|
|
+
|
|
|
+ if user is None or value is None:
|
|
|
+ bot.say('Du bist eine Pappnase - User und Knoten, bitte.')
|
|
|
+ return
|
|
|
+
|
|
|
+ user = str(user)
|
|
|
+ print('NodeACL ' + cmd + ' \'' + value + '\' for user \'' + user + '\'')
|
|
|
+ uid = [ x for x in nodeaccess if x == user or x.lower() == user ]
|
|
|
+ if cmd == 'add':
|
|
|
+ uid = uid[0] if len(uid) > 0 else user
|
|
|
+ if not uid in nodeaccess:
|
|
|
+ nodeaccess[uid] = []
|
|
|
+ if not value in nodeaccess[uid]:
|
|
|
+ nodeaccess[uid].append(value)
|
|
|
+ bot.say('201 nodeACL \'{0}\' +\'{1}\''.format(uid, value))
|
|
|
+ else:
|
|
|
+ bot.say('304 nodeACL \'{0}\' contains \'{1}\''.format(uid, value))
|
|
|
+
|
|
|
+ elif cmd == 'del':
|
|
|
+ if len(uid) == 0:
|
|
|
+ bot.say('404 nodeACL \'{0}\''.format(uid))
|
|
|
+ return
|
|
|
+ if value in nodeaccess[uid]:
|
|
|
+ nodeaccess[uid].remove(value)
|
|
|
+ bot.say('200 nodeACL \'{0}\' -\'{1}\''.format(uid, value))
|
|
|
+ else:
|
|
|
+ bot.say('404 nodeACL \'{0}\' does not contain \'{1}\''.format(uid, value))
|
|
|
+
|
|
|
+ elif cmd == 'check':
|
|
|
+ if len(uid) == 0:
|
|
|
+ bot.say('Nope, keine ACL gesetzt.')
|
|
|
+ return
|
|
|
+
|
|
|
+ node = ffpb_findnode(value)
|
|
|
+ if node is None:
|
|
|
+ bot.say('Nope, kein Plan was für ein Knoten das ist.')
|
|
|
+ return
|
|
|
+
|
|
|
+ result = playitsafe(bot, trigger, debug_user=uid[0], debug_ignorebotadmin=True, node=node, reply_directly=False)
|
|
|
+ if result == True:
|
|
|
+ bot.say('Jupp.')
|
|
|
+ elif result == False:
|
|
|
+ bot.say('Nope.')
|
|
|
+ else:
|
|
|
+ bot.say('Huh? result=' + str(result))
|
|
|
+
|
|
|
+ return
|
|
|
+
|
|
|
+ bot.say('Unbekanntes Kommando. Probier "list [user]", "add user value" oder "del user value". Value kann node_id oder hostname-Maske sein.')
|
|
|
+
|
|
|
def ffpb_findnode(name):
|
|
|
"""helper: try to identify the node the user meant by the given name"""
|
|
|
|
|
@@ -397,21 +548,16 @@ def ffpb_debug_alfred(bot, trigger):
|
|
|
def ffpb_peerdata(bot, trigger):
|
|
|
"""Show ALFRED data of the given node."""
|
|
|
|
|
|
- # user must be a bot admin
|
|
|
- if (not trigger.admin):
|
|
|
- bot.say('I wont leak (possibly) sensitive data to you.')
|
|
|
- return
|
|
|
-
|
|
|
- # query must be a PM or as OP in the channel
|
|
|
- if (not trigger.is_privmsg) and (not trigger.nick in bot.ops[trigger.sender]):
|
|
|
- bot.say('Kein Keks? Keine Daten.')
|
|
|
- return
|
|
|
-
|
|
|
# identify node or bail out
|
|
|
target_name = trigger.group(2)
|
|
|
node = ffpb_findnode_from_botparam(bot, target_name)
|
|
|
if node is None: return
|
|
|
|
|
|
+ # query must be a PM or as OP in the channel
|
|
|
+ if not playitsafe(bot, trigger, need_op=True, node=node):
|
|
|
+ # the check function already gives a bot reply, just exit here
|
|
|
+ return
|
|
|
+
|
|
|
# reply each key in the node's data
|
|
|
for key in node:
|
|
|
if key in [ 'hostname' ]: continue
|
|
@@ -938,25 +1084,14 @@ def ffpb_remoteexec(bot, trigger):
|
|
|
target_name = bot_params[0]
|
|
|
target_cmd = bot_params[1]
|
|
|
|
|
|
- # remote execution may only be triggered by bot admins
|
|
|
- if not trigger.admin:
|
|
|
- bot.say('I can haz sudo?')
|
|
|
- return
|
|
|
-
|
|
|
- # make sure remote execution is done in public
|
|
|
- if trigger.is_privmsg:
|
|
|
- bot.say('Bitte per Channel.')
|
|
|
- return
|
|
|
-
|
|
|
- # double-safety: user must be op in the channel, too (hoping for NickServ authentication)
|
|
|
- if not trigger.nick in bot.ops[trigger.sender]:
|
|
|
- bot.say('Geh weg.')
|
|
|
- return
|
|
|
-
|
|
|
# identify requested node or bail out
|
|
|
node = ffpb_findnode_from_botparam(bot, target_name, ensure_recent_alfreddata=False)
|
|
|
if node is None: return
|
|
|
|
|
|
+ # check ACL
|
|
|
+ if not playitsafe(bot, trigger, via_channel=True, node=node):
|
|
|
+ return
|
|
|
+
|
|
|
# use the node's first non-linklocal address
|
|
|
target = [x for x in node["network"]["addresses"] if not x.lower().startswith("fe80:")][0]
|
|
|
target_alias = node["hostname"]
|