# -*- coding: utf-8 -*- from __future__ import print_function import willie import git import netaddr import json import urllib2 import re import os import subprocess import dns.resolver,dns.reversename import socket import SocketServer import threading msgserver = None peers_repo = None stats = None alfred_method = None alfred_data = None ffpb_resolver = dns.resolver.Resolver () ffpb_resolver.nameservers = ['10.132.254.53'] class MsgHandler(SocketServer.BaseRequestHandler): def handle(self): data = self.request.recv(2048).strip() sender = self._resolve_name (self.client_address[0]) bot = self.server.bot if bot is None: print("ERROR: No bot in handle() :-(") return target = bot.config.core.owner if bot.config.has_section('ffpb'): is_public = data.lstrip().lower().startswith("public:") if is_public and not (bot.config.ffpb.msg_target_public is None): data = data[7:].lstrip() target = bot.config.ffpb.msg_target_public elif not (bot.config.ffpb.msg_target is None): target = bot.config.ffpb.msg_target bot.msg(target, "[{0}] {1}".format(sender, str(data))) def _resolve_name (self, ip): if ip.startswith ("127."): return "localhost" try: addr = dns.reversename.from_address (ip) return re.sub ("(.infra)?.ffpb.", "", str (ffpb_resolver.query (addr, "PTR")[0])) except dns.resolver.NXDOMAIN: return ip class ThreadingTCPServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer): pass def setup(bot): global msgserver, peers_repo, alfred_method if not bot.config.has_section('ffpb'): return if not bot.config.ffpb.peers_directory is None: peers_repo = git.Repo(bot.config.ffpb.peers_directory) assert peers_repo.bare == False if int(bot.config.ffpb.msg_enable) == 1: host = "localhost" port = 2342 if not bot.config.ffpb.msg_host is None: host = bot.config.ffpb.msg_host if not bot.config.ffpb.msg_port is None: port = int(bot.config.ffpb.msg_port) msgserver = ThreadingTCPServer((host,port), MsgHandler) msgserver.bot = bot ip, port = msgserver.server_address print("Messaging server listening on {}:{}".format(ip,port)) msgserver_thread = threading.Thread(target=msgserver.serve_forever) msgserver_thread.daemon = True msgserver_thread.start() alfred_method = bot.config.ffpb.alfred_method ffpb_updatealfred(bot) def shutdown(bot): global msgserver if not msgserver is None: msgserver.shutdown() print("Closed messaging server.") msgserver = None def ffpb_findnode(name): if name is None or len(name) == 0: return None name = str(name).strip() # try to match MAC m = re.search("^([0-9a-fA-F][0-9a-fA-F]:){5}[0-9a-fA-F][0-9a-fA-F]$", name) if (not m is None): mac = m.group(0).lower() if mac in alfred_data: return alfred_data[mac] # try to find alias MAC for nodeid in alfred_data: node = alfred_data[nodeid] if "network" in node: if "mac" in node["network"] and node["network"]["mac"].lower() == mac: return node if "mesh_interfaces" in node["network"]: for mim in node["network"]["mesh_interfaces"]: if mim.lower() == mac: return node # look through the ALFRED peers possible_matches = [] for nodeid in alfred_data: node = alfred_data[nodeid] if "hostname" in node and node["hostname"].lower() == name.lower(): return node # still not found -> try peers_repo if not peers_repo is None: peer_name = None peer_mac = None peer_file = None for b in peers_repo.heads.master.commit.tree.blobs: if b.name.lower() == name.lower(): peer_name = b.name peer_file = b.abspath break if (not peer_file is None) and os.path.exists(peer_file): peerfile = open(peer_file, "r") for line in peerfile: if line.startswith("# MAC:"): peer_mac = line[6:].strip() peerfile.close() if not (peer_mac is None): return { "hostname": peer_name, "network": { "addresses": [ mac2ipv6(peer_mac, "fdca:ffee:ff12:132:") ], "mac": peer_mac } } return None def mac2ipv6(mac, prefix=None): result = str(netaddr.EUI(mac).ipv6_link_local()) if (not prefix is None) and (result.startswith("fe80::")): result = prefix + result[6:] return result @willie.module.interval(30) def ffpb_updatealfred(bot): """Aktualisiere ALFRED-Daten""" global alfred_data if alfred_method is None or alfred_method == "None": return if alfred_method == "exec": rawdata = subprocess.check_output(['alfred-json', '-z', '-r', '158']) alfred_data = json.load(rawdata) #print("Fetched new ALFRED data:", len(alfred_data), "entries") return if alfred_method.startswith("http"): rawdata = urllib2.urlopen(alfred_method) alfred_data = json.load(rawdata) #print("Downloaded new ALFRED data:", len(alfred_data), "entries") return print("Unknown ALFRED data method '", alfred_method, "', cannot load new data.", sep="") alfred_data = None @willie.module.commands('info') def ffpb_peerinfo(bot, trigger): target_name = trigger.group(2) if (target_name is None or len(target_name) == 0): bot.say(str(trigger.nick + ": Grün.")) return if alfred_data is None: bot.say("Informationen sind ausverkauft, kommen erst morgen wieder rein.") return node = ffpb_findnode(target_name) if node is None: bot.say("Kein Plan wer oder was mit '" + target_name + "' gemeint ist :(") return info_mac = node["network"]["mac"] info_name = node["hostname"] info_hw = "" if "hardware" in node: if "model" in node["hardware"]: model = node["hardware"]["model"] info_hw = " model='" + model + "'" info_fw = "" info_update = "" if "software" in node: if "firmware" in node["software"]: fwinfo = str(node["software"]["firmware"]["release"]) if "release" in node["software"]["firmware"] else "unknown" info_fw = " firmware=" + fwinfo if "autoupdater" in node["software"]: autoupdater = node["software"]["autoupdater"]["branch"] if node["software"]["autoupdater"]["enabled"] else "off" info_update = " (autoupdater="+autoupdater+")" info_uptime = "" if "statistics" in node and "uptime" in node["statistics"]: u = int(float(node["statistics"]["uptime"])) d, r1 = divmod(int(float(node["statistics"]["uptime"])), 86400) h, r2 = divmod(r1, 3600) m, s = divmod(r2, 60) if d > 0: info_uptime = ' up {0}d {1}h'.format(d,h) elif h > 0: info_uptime = ' up {0}h {1}m'.format(h,m) else: info_uptime = ' up {0}m'.format(m) bot.say('[{1}]{2}{3}{4}{5}'.format(info_mac, info_name, info_hw, info_fw, info_update, info_uptime)) @willie.module.commands('link') def ffpb_peerlink(bot, trigger): target_name = trigger.group(2) if (target_name is None or len(target_name) == 0): bot.say(str(trigger.nick + ": Grün.")) return if alfred_data is None: bot.say("Informationen sind ausverkauft, kommen erst morgen wieder rein.") return node = ffpb_findnode(target_name) if node is None: bot.say("Kein Plan wer oder was mit '" + target_name + "' gemeint ist :(") return info_mac = node["network"]["mac"] info_name = node["hostname"] info_v6 = mac2ipv6(info_mac, 'fdca:ffee:ff12:132:') bot.say('[{1}] mac {0} -> http://[{2}]/'.format(info_mac, info_name, info_v6)) @willie.module.interval(60) def ffpb_updatepeers(bot): """Aktualisiere die Knotenliste und melde das Diff""" if peers_repo is None: print('WARNING: peers_repo is None') return old_head = peers_repo.head.commit peers_repo.remotes.origin.pull() new_head = peers_repo.head.commit if new_head != old_head: print('git pull: from ' + str(old_head) + ' to ' + str(new_head)) added = [] changed = [] renamed = [] deleted = [] for f in old_head.diff(new_head): if f.new_file: added.append(f.b_blob.name) elif f.deleted_file: deleted.append(f.a_blob.name) elif f.renamed: renamed.append([f.rename_from, f.rename_to]) else: changed.append(f.a_blob.name) response = "Knoten-Update (VPN +{0} %{1} -{2}): ".format(len(added), len(renamed)+len(changed), len(deleted)) for f in added: response += " +'{}'".format(f) for f in changed: response += " %'{}'".format(f) for f in renamed: response += " '{}'->'{}'".format(f[0],f[1]) for f in deleted: response += " -'{}'".format(f) bot.msg(bot.config.ffpb.msg_target, response) @willie.module.interval(15) def ffpb_get_stats(bot): global stats response = urllib2.urlopen('http://map.paderborn.freifunk.net/nodes.json') data = json.load(response) nodes_active = 0 nodes_total = 0 clients_count = 0 for node in data['nodes']: if node['flags']['gateway'] or node['flags']['client']: continue nodes_total += 1 if node['flags']['online']: nodes_active += 1 for link in data['links']: if link['type'] == 'client': clients_count += 1 if stats is None: stats = { } stats["nodes_active"] = nodes_active stats["nodes_total"] = nodes_total stats["clients"] = clients_count @willie.module.commands('status') def ffpb_status(bot, trigger): """Status des FFPB-Netzes: Anzahl (aktiver) Knoten + Clients""" if stats is None: bot.say('Uff, kein Plan wo der Zettel ist. Fragst du später nochmal?') return bot.say('Es sind {0} Knoten und ca. {1} Clients online.'.format(stats["nodes_active"], stats["clients"])) @willie.module.commands('ping') def ffpb_ping(bot, trigger): """Ping FFPB-Knoten""" target_name = trigger.group(2) if target_name is None or len(target_name) == 0: bot.say('Alter, wen soll ich denn pingen? Einmal mit Profis arbeiten -.-') return node = ffpb_findnode(target_name) if node is None: bot.say('Kein Plan wer mit \'' + target_name + '\' gemeint ist :/') return target = node["network"]["addresses"][0] target_alias = node["hostname"] print("ping '", target , '"', sep='') result = os.system('ping6 -c 2 -W 1 ' + target + ' 2>/dev/null') if result == 0: bot.say('Knoten "' + target_alias + '" antwortet \o/') elif result == 1 or result == 256: bot.say('Keine Antwort von "' + target_alias + '" :-(') else: bot.say('Uh oh, irgendwas ist kaputt. Chef, ping result = ' + str(result) + ' - darf ich das essen?') @willie.module.commands('exec-on-peer') def ffpb_remoteexec(bot, trigger): """Remote Execution fuer FFPB_Knoten""" bot_params = trigger.group(2).split(' ',1) if len(bot_params) != 2: bot.say('Wenn du nicht sagst wo mach ich remote execution bei dir!') bot.say('Tipp: !exec-on-peer ') return target_name = bot_params[0] target_cmd = bot_params[1] if not trigger.admin: bot.say('I can haz sudo?') return if trigger.is_privmsg: bot.say('Bitte per Channel.') return if not trigger.nick in bot.ops[trigger.sender]: bot.say('Geh weg.') return node = ffpb_findnode(target_name) if node is None: bot.say('Kein Plan wer mit \'' + target_name + '\' gemeint ist :/') return target = node["network"]["addresses"][0] target_alias = node["hostname"] cmd = 'ssh -6 -l root ' + target + ' -- "' + target_cmd + '"' print("REMOTE EXEC = " + cmd) try: result = subprocess.check_output(['ssh', '-6n', '-l', 'root', '-o', 'BatchMode=yes', '-o','StrictHostKeyChecking=no', target, target_cmd], stderr=subprocess.STDOUT, shell=False) lines = str(result).splitlines() if len(lines) == 0: bot.say('exec-on-peer(' + target_alias + '): No output') return msg = 'exec-on-peer(' + target_alias + '): ' + str(len(lines)) + ' Zeilen' if len(lines) > 8: msg += ' (zeige max. 8)' bot.say(msg + ':') for line in lines[0:8]: bot.say(line) except subprocess.CalledProcessError as e: bot.say('Fehler '+str(e.returncode)+' bei exec-on-peer('+target_alias+'): ' + e.output)