ffpb.py 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882
  1. # -*- coding: utf-8 -*-
  2. from __future__ import print_function
  3. import willie
  4. from datetime import datetime, timedelta
  5. import difflib
  6. from email.utils import mktime_tz
  7. from fnmatch import fnmatch
  8. import git
  9. import netaddr
  10. import json
  11. import urllib2
  12. import re
  13. import os
  14. import random
  15. import shelve
  16. import subprocess
  17. import time
  18. import dns.resolver, dns.reversename
  19. import SocketServer
  20. import threading
  21. msgserver = None
  22. peers_repo = None
  23. nodeaccess = None
  24. alfred_method = None
  25. ffpb_resolver = dns.resolver.Resolver ()
  26. ffpb_resolver.nameservers = ['10.132.254.53']
  27. class MsgHandler(SocketServer.BaseRequestHandler):
  28. """Reads line from TCP stream and forwards it to configured IRC channels."""
  29. def handle(self):
  30. data = self.request.recv(2048).strip()
  31. sender = self.resolve_name(self.client_address[0])
  32. bot = self.server.bot
  33. if bot is None:
  34. print("ERROR: No bot in handle() :-(")
  35. return
  36. target = bot.config.core.owner
  37. if bot.config.has_section('ffpb'):
  38. is_public = data.lstrip().lower().startswith("public:")
  39. if is_public and not bot.config.ffpb.msg_target_public is None:
  40. data = data[7:].lstrip()
  41. target = bot.config.ffpb.msg_target_public
  42. elif not bot.config.ffpb.msg_target is None:
  43. target = bot.config.ffpb.msg_target
  44. bot.msg(target, "[{0}] {1}".format(sender, str(data)))
  45. def resolve_name(self, ipaddr):
  46. """
  47. Resolves the host name of the given IP address
  48. and strips away the suffix (.infra)?.ffpb
  49. """
  50. if ipaddr.startswith("127."):
  51. return "localhost"
  52. try:
  53. addr = dns.reversename.from_address(ipaddr)
  54. return re.sub("(.infra)?.ffpb.", "", str(ffpb_resolver.query(addr, "PTR")[0]))
  55. except dns.resolver.NXDOMAIN:
  56. return ipaddr
  57. class ThreadingTCPServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer):
  58. """Defines a threaded TCP socket server."""
  59. bot = None
  60. def setup(bot):
  61. """Called by willie upon loading this plugin."""
  62. global msgserver, peers_repo, alfred_method, nodeaccess
  63. # signal begin of setup routine
  64. bot.memory['ffpb_in_setup'] = True
  65. # load list of seen nodes from disk
  66. seen_nodes = shelve.open('nodes.seen', writeback=True)
  67. bot.memory['seen_nodes'] = seen_nodes
  68. # load list of node ACL from disk (used in playitsafe())
  69. nodeaccess = shelve.open('nodes.acl', writeback=True)
  70. # no need to configure anything else if the ffpb config section is missing
  71. if not bot.config.has_section('ffpb'):
  72. bot.memory['ffpb_in_setup'] = False
  73. return
  74. # open the git repository containing the peers files
  75. if not bot.config.ffpb.peers_directory is None:
  76. peers_repo = git.Repo(bot.config.ffpb.peers_directory)
  77. assert peers_repo.bare == False
  78. # if configured, start the messaging server
  79. if int(bot.config.ffpb.msg_enable) == 1:
  80. host = "localhost"
  81. port = 2342
  82. if not bot.config.ffpb.msg_host is None:
  83. host = bot.config.ffpb.msg_host
  84. if not bot.config.ffpb.msg_port is None:
  85. port = int(bot.config.ffpb.msg_port)
  86. msgserver = ThreadingTCPServer((host, port), MsgHandler)
  87. msgserver.bot = bot
  88. ipaddr, port = msgserver.server_address
  89. print("Messaging server listening on {}:{}".format(ipaddr, port))
  90. msgserver_thread = threading.Thread(target=msgserver.serve_forever)
  91. msgserver_thread.daemon = True
  92. msgserver_thread.start()
  93. # initially fetch ALFRED data
  94. alfred_method = bot.config.ffpb.alfred_method
  95. if not 'alfred_data' in bot.memory:
  96. bot.memory['alfred_data'] = {}
  97. if not 'alfred_update' in bot.memory:
  98. bot.memory['alfred_update'] = datetime(1970, 1, 1, 23, 42)
  99. ffpb_updatealfred(bot)
  100. # signal end of setup routine
  101. bot.memory['ffpb_in_setup'] = False
  102. def shutdown(bot):
  103. global msgserver, nodeaccess
  104. # store node acl
  105. if not nodeaccess is None:
  106. nodeaccess.sync()
  107. nodeaccess.close()
  108. nodeaccess = None
  109. # store seen nodes
  110. if 'seen_nodes' in bot.memory and bot.memory['seen_nodes'] != None:
  111. bot.memory['seen_nodes'].close()
  112. bot.memory['seen_nodes'] = None
  113. del bot.memory['seen_nodes']
  114. # shutdown messaging server
  115. if not msgserver is None:
  116. msgserver.shutdown()
  117. print("Closed messaging server.")
  118. msgserver = None
  119. @willie.module.commands("help")
  120. @willie.module.commands("hilfe")
  121. @willie.module.commands("man")
  122. def ffpb_help(bot, trigger):
  123. """Display commony ulsed functions."""
  124. functions = {
  125. "!ping <knoten>": "Prüfe ob der Knoten erreichbar ist.",
  126. "!status": "Aktuellen Status des Netzwerks (insb. Anzahl Knoten und Clients) ausgegeben.",
  127. "!info <knoten>": "Allgemeine Information zu dem Knoten anzeigen.",
  128. "!link <knoten>": "MAC-Adresse und Link zur Status-Seite des Knotens anzeigen.",
  129. "!exec-on-peer <knoten> <kommando>": "Befehl auf dem Knoten ausführen (nur möglich bei eigenen Knoten oder als Admin, in beiden Fällen auch nur wenn der SSH-Key des Bots hinterlegt wurde)",
  130. "!mesh <knoten>": "Zeige Mesh-Partner eines Knotens",
  131. }
  132. param = trigger.group(2)
  133. if param is None:
  134. bot.say("Funktionen: " + str.join(", ", sorted(functions.keys())))
  135. return
  136. if param.startswith("!"):
  137. param = param[1:]
  138. for fun in functions.keys():
  139. if fun.startswith("!" + param + " "):
  140. bot.say("Hilfe zu '" + fun + "': " + functions[fun])
  141. return
  142. bot.say("Allgemeine Hilfe gibt's mit !help - ohne Parameter.")
  143. def playitsafe(bot, trigger,
  144. botadmin=False, admin_channel=False, via_channel=False, via_privmsg=False, need_op=False, node=None,
  145. reply_directly=True, debug_user=None, debug_ignorebotadmin=False):
  146. """
  147. helper: checks that the triggering user has the necessary rights
  148. Returns true if everything is okay.
  149. If it's not, a reply is send via the bot and false is returned.
  150. """
  151. if via_channel and via_privmsg:
  152. raise Exception('Der Entwickler ist ein dummer, dummer Junge ' +
  153. '(playitsafe hat via_channel + via_privmsg gleichzeitig gesetzt).')
  154. user = trigger.nick if debug_user is None else debug_user
  155. user = user.lower()
  156. # botadmin: you need to be configured as a bot admin
  157. if botadmin and not trigger.admin:
  158. if reply_directly:
  159. bot.say('Du brauchst Super-Kuh-Kräfte um dieses Kommando auszuführen.')
  160. return False
  161. # via_channel: the request must not be a private conversation
  162. if via_channel and trigger.is_privmsg:
  163. if reply_directly:
  164. bot.say('Bitte per Channel - mehr Transparenz wagen und so!')
  165. return False
  166. # via_privmsg: the request must be a private conversation
  167. if via_privmsg and not trigger.is_privmsg:
  168. if reply_directly:
  169. bot.say('Solche Informationen gibt es nur per PM, da bin ich ja schon ein klein wenig sensibel ...')
  170. return False
  171. # need_op: if the message is in a channel, check that the user has OP there
  172. if need_op and (not trigger.is_privmsg) and (not user in bot.ops[trigger.sender]):
  173. if reply_directly:
  174. bot.say('Keine Zimtschnecke, keine Kekse.')
  175. return False
  176. # node: check that the user is whitelisted (or is admin)
  177. if not node is None and (debug_ignorebotadmin or not trigger.admin):
  178. acluser = [x for x in nodeaccess if x.lower() == user]
  179. acluser = acluser[0] if len(acluser) == 1 else None
  180. if nodeaccess is None or acluser is None:
  181. if reply_directly:
  182. bot.reply('You! Shall! Not! Access!')
  183. return False
  184. nodeid = node['node_id'] if 'node_id' in node else None
  185. matched = False
  186. for x in nodeaccess[acluser]:
  187. if x == nodeid or fnmatch(node['hostname'], x):
  188. matched = True
  189. break
  190. if not matched:
  191. if reply_directly:
  192. bot.reply('Mach das doch bitte auf deinen Knoten, kthxbye.')
  193. return False
  194. return True
  195. @willie.module.commands('nodeacl')
  196. def ffpb_nodeacl(bot, trigger):
  197. """Configure ACL for nodes."""
  198. if not playitsafe(bot, trigger, botadmin=True):
  199. # the check function already gives a bot reply, just exit here
  200. return
  201. # ensure the user gave arguments
  202. if trigger.group(2) is None or len(trigger.group(2)) == 0:
  203. bot.say('Sag doch was du willst ... einmal mit Profis arbeiten, ey -.-')
  204. return
  205. # read additional arguments
  206. cmd = trigger.group(3).lower()
  207. if cmd == 'list':
  208. user = trigger.group(4)
  209. if user is None:
  210. usernames = [x for x in nodeaccess]
  211. bot.say('ACLs gesetzt für die User: ' + ', '.join(usernames))
  212. return
  213. user = user.lower()
  214. uid = [x for x in nodeaccess if x.lower() == user]
  215. if len(uid) == 0:
  216. bot.say('Für \'{0}\' ist keine Node ACL gesetzt.'.format(user))
  217. return
  218. bot.say('Node ACL für \'{0}\' = \'{1}\''.format(
  219. uid[0],
  220. '\', \''.join(nodeaccess[uid[0]]))
  221. )
  222. return
  223. if cmd in ['add', 'del', 'check']:
  224. user = trigger.group(4)
  225. value = trigger.group(5)
  226. if user is None or value is None:
  227. bot.say('Du bist eine Pappnase - User und Knoten, bitte.')
  228. return
  229. user = str(user)
  230. print('NodeACL ' + cmd + ' \'' + value + '\' for user \'' + user + '\'')
  231. uid = [x for x in nodeaccess if x == user or x.lower() == user]
  232. if cmd == 'add':
  233. uid = uid[0] if len(uid) > 0 else user
  234. if not uid in nodeaccess:
  235. nodeaccess[uid] = []
  236. if not value in nodeaccess[uid]:
  237. nodeaccess[uid].append(value)
  238. bot.say('201 nodeACL \'{0}\' +\'{1}\''.format(uid, value))
  239. else:
  240. bot.say('304 nodeACL \'{0}\' contains \'{1}\''.format(uid, value))
  241. elif cmd == 'del':
  242. if len(uid) == 0:
  243. bot.say('404 nodeACL \'{0}\''.format(uid))
  244. return
  245. if value in nodeaccess[uid]:
  246. nodeaccess[uid].remove(value)
  247. bot.say('200 nodeACL \'{0}\' -\'{1}\''.format(uid, value))
  248. else:
  249. bot.say('404 nodeACL \'{0}\' does not contain \'{1}\''.format(uid, value))
  250. elif cmd == 'check':
  251. if len(uid) == 0:
  252. bot.say('Nope, keine ACL gesetzt.')
  253. return
  254. node = ffpb_findnode(value)
  255. if node is None:
  256. bot.say('Nope, kein Plan was für ein Knoten das ist.')
  257. return
  258. result = playitsafe(bot, trigger, debug_user=uid[0], debug_ignorebotadmin=True, node=node, reply_directly=False)
  259. if result == True:
  260. bot.say('Jupp.')
  261. elif result == False:
  262. bot.say('Nope.')
  263. else:
  264. bot.say('Huh? result=' + str(result))
  265. return
  266. bot.say('Unbekanntes Kommando. Probier "list [user]", "add user value" oder "del user value". Value kann node_id oder hostname-Maske sein.')
  267. def ffpb_ensurenodeid(nodedata):
  268. """Makes sure that the given dict has a 'node_id' field."""
  269. if 'node_id' in nodedata:
  270. return nodedata
  271. # derive node's id
  272. nodeid = nodedata['network']['mac'].replace(':', '') if 'network' in nodedata and 'mac' in nodedata['network'] else None
  273. # assemble extended data
  274. result = {'node_id': nodeid}
  275. for key in nodedata:
  276. result[key] = nodedata[key]
  277. return result
  278. def ffpb_findnode(name, alfred_data=None, allow_fuzzymatching=True):
  279. """helper: try to identify the node the user meant by the given name"""
  280. # no name, no node
  281. if name is None or len(name) == 0:
  282. return None
  283. name = str(name).strip()
  284. # disable fuzzy matching if name is enclosed in quotes
  285. if name.startswith('\'') and name.endswith('\'') or \
  286. name.startswith('"') and name.endswith('"'):
  287. name = name[1:-1]
  288. allow_fuzzymatching = False
  289. names = {}
  290. if not alfred_data is None:
  291. # try to match MAC
  292. m = re.search("^([0-9a-fA-F][0-9a-fA-F]:){5}[0-9a-fA-F][0-9a-fA-F]$", name)
  293. if not m is None:
  294. mac = m.group(0).lower()
  295. if mac in alfred_data:
  296. return ffpb_ensurenodeid(alfred_data[mac])
  297. # try to find alias MAC in ALFRED data
  298. for nodeid in alfred_data:
  299. node = alfred_data[nodeid]
  300. if "network" in node:
  301. if "mac" in node["network"] and node["network"]["mac"].lower() == mac:
  302. return ffpb_ensurenodeid(node)
  303. if "mesh_interfaces" in node["network"]:
  304. for mim in node["network"]["mesh_interfaces"]:
  305. if mim.lower() == mac:
  306. return ffpb_ensurenodeid(node)
  307. nodeid = mac.replace(':', '').lower()
  308. return {
  309. 'nodeid': nodeid,
  310. 'hostname': '?-' + nodeid,
  311. 'network': {
  312. 'addresses': [mac2ipv6(mac, 'fdca:ffee:ff12:132:')],
  313. 'mac': mac,
  314. },
  315. 'hardware': {
  316. 'model': 'derived-from-mac',
  317. },
  318. }
  319. # look through the ALFRED peers
  320. for nodeid in alfred_data:
  321. node = alfred_data[nodeid]
  322. if 'hostname' in node:
  323. h = node['hostname']
  324. if h.lower() == name.lower():
  325. return node
  326. else:
  327. names[h] = nodeid
  328. # not found in ALFRED data -> try peers_repo
  329. if not peers_repo is None:
  330. peer_name = None
  331. peer_mac = None
  332. peer_file = None
  333. for b in peers_repo.heads.master.commit.tree.blobs:
  334. if b.name.lower() == name.lower():
  335. peer_name = b.name
  336. peer_file = b.abspath
  337. break
  338. if (not peer_file is None) and os.path.exists(peer_file):
  339. peerfile = open(peer_file, "r")
  340. for line in peerfile:
  341. if line.startswith("# MAC:"):
  342. peer_mac = line[6:].strip()
  343. peerfile.close()
  344. if not peer_mac is None:
  345. return {
  346. 'node_id': peer_mac.replace(':', ''),
  347. 'hostname': peer_name,
  348. 'network': {
  349. 'addresses': [mac2ipv6(peer_mac, 'fdca:ffee:ff12:132:'),],
  350. 'mac': peer_mac,
  351. },
  352. 'hardware': {
  353. 'model': 'derived-from-vpnkeys',
  354. },
  355. }
  356. # do a similar name lookup in the ALFRED data
  357. if allow_fuzzymatching and not alfred_data is None:
  358. allnames = [x for x in names]
  359. possibilities = difflib.get_close_matches(name, allnames, cutoff=0.75)
  360. print('findnode: Fuzzy matching \'{0}\' got {1} entries: {2}'.format(
  361. name,
  362. len(possibilities), ', '.join(possibilities))
  363. )
  364. if len(possibilities) == 1:
  365. # if we got exactly one candidate that might be it
  366. return ffpb_ensurenodeid(alfred_data[names[possibilities[0]]])
  367. # none of the above was able to identify the requested node
  368. return None
  369. def ffpb_findnode_from_botparam(bot, name, ensure_recent_alfreddata=True):
  370. """helper: call ffpb_findnode() and give common answers via bot if nothing has been found"""
  371. if name is None or len(name) == 0:
  372. if not bot is None:
  373. bot.reply("Grün.")
  374. return None
  375. alfred_data = get_alfred_data(bot, ensure_recent_alfreddata)
  376. if ensure_recent_alfreddata and alfred_data is None:
  377. if not bot is None:
  378. bot.say('Informationen sind ausverkauft bzw. veraltet, ' +
  379. 'daher sage ich mal lieber nichts zu \'' + name + '\'.')
  380. return None
  381. node = ffpb_findnode(name, alfred_data)
  382. if node is None:
  383. if not bot is None:
  384. bot.say("Kein Plan wer oder was mit '" + name + "' gemeint ist :(")
  385. return node
  386. def mac2ipv6(mac, prefix=None):
  387. """Calculate IPv6 address from given MAC,
  388. optionally replacing the fe80:: prefix with a given one."""
  389. result = str(netaddr.EUI(mac).ipv6_link_local())
  390. if (not prefix is None) and (result.startswith("fe80::")):
  391. result = prefix + result[6:]
  392. return result
  393. @willie.module.interval(30)
  394. def ffpb_updatealfred(bot):
  395. """Aktualisiere ALFRED-Daten"""
  396. if alfred_method is None or alfred_method == "None":
  397. return
  398. updated = None
  399. if alfred_method == "exec":
  400. rawdata = subprocess.check_output(['alfred-json', '-z', '-r', '158'])
  401. updated = datetime.now()
  402. elif alfred_method.startswith("http"):
  403. try:
  404. rawdata = urllib2.urlopen(alfred_method)
  405. except urllib2.URLError as err:
  406. print("Failed to download ALFRED data:" + str(err))
  407. return
  408. last_modified = rawdata.info().getdate_tz("Last-Modified")
  409. updated = datetime.fromtimestamp(mktime_tz(last_modified))
  410. else:
  411. print("Unknown ALFRED data method '{0}', cannot load new data.".format(alfred_method))
  412. alfred_data = None
  413. return
  414. try:
  415. alfred_data = json.load(rawdata)
  416. #print("Fetched new ALFRED data:", len(alfred_data), "entries")
  417. except ValueError as err:
  418. print("Failed to parse ALFRED data: " + str(err))
  419. return
  420. bot.memory['alfred_data'] = alfred_data
  421. bot.memory['alfred_update'] = updated
  422. seen_nodes = bot.memory['seen_nodes'] if 'seen_nodes' in bot.memory else None
  423. if not seen_nodes is None:
  424. new = []
  425. for nodeid in alfred_data:
  426. nodeid = str(nodeid)
  427. if not nodeid in seen_nodes:
  428. seen_nodes[nodeid] = updated
  429. new.append((nodeid, alfred_data[nodeid]['hostname']))
  430. print('First time seen: ' + str(nodeid))
  431. if len(new) > 0 and not bot.memory['ffpb_in_setup']:
  432. action_msg = None
  433. if len(new) == 1:
  434. action_msg = random.choice((
  435. 'bemerkt den neuen Knoten {0}',
  436. 'entdeckt {0}',
  437. 'reibt sich die Augen und erblickt einen verpackungsfrischen Knoten {0}',
  438. u'heißt {0} im Mesh willkommen',
  439. 'freut sich, dass {0} aufgetaucht ist',
  440. 'traut seinen Augen kaum. {0} sagt zum ersten Mal: Hallo Freifunk Paderborn',
  441. u'sieht die ersten Herzschläge von {0}',
  442. u'stellt einen großen Pott Heißgetränk zu {0} und fragt ob es hier Meshpartner gibt.',
  443. )).format('\'' + str(new[0][1]) + '\'')
  444. else:
  445. action_msg = random.choice((
  446. 'bemerkt die neuen Knoten {0} und {1}',
  447. 'hat {0} und {1} entdeckt',
  448. 'bewundert {0} sowie {1}',
  449. 'freut sich, dass {0} und {1} nun auch online sind',
  450. u'heißt {0} und {1} im Mesh willkommen',
  451. 'fragt sich ob die noch jungen Herzen von {0} und {1} synchron schlagen',
  452. ))
  453. all_but_last = [str(x[1]) for x in new[0:-1]]
  454. last = str(new[-1][1])
  455. action_msg = action_msg.format(
  456. '\'' + '\', \''.join(all_but_last) + '\'',
  457. '\'' + last + '\''
  458. )
  459. action_target = bot.config.ffpb.msg_target
  460. if not bot.config.ffpb.msg_target_public is None:
  461. action_target = bot.config.ffpb.msg_target_public
  462. bot.msg(action_target, '\x01ACTION %s\x01' % action_msg)
  463. def get_alfred_data(bot, ensure_not_outdated=True):
  464. """
  465. Retrieves the stored alfred_data and optionally checks
  466. that it has been updated no more than 5 minutes ago.
  467. """
  468. alfred_data = bot.memory['alfred_data'] if 'alfred_data' in bot.memory else None
  469. alfred_update = bot.memory['alfred_update'] if 'alfred_update' in bot.memory else 0
  470. if alfred_data is None:
  471. return None
  472. if ensure_not_outdated:
  473. timeout = datetime.now() - timedelta(minutes=5)
  474. is_outdated = timeout > alfred_update
  475. if is_outdated:
  476. return None
  477. return alfred_data
  478. def ffpb_get_batcave_nodefield(nodeid, field):
  479. """Query the given field for the given nodeid from the BATCAVE."""
  480. raw_data = None
  481. try:
  482. # query BATCAVE for node's field
  483. raw_data = urllib2.urlopen('http://[fdca:ffee:ff12:a255::253]:8888/node/{0}/{1}'.format(nodeid, field))
  484. except urllib2.URLError as err:
  485. print('Failed to contact BATCAVE for \'{0}\'->\'{1}\': {2}'.format(nodeid, field, err))
  486. return None
  487. try:
  488. return json.load(raw_data)
  489. except ValueError as err:
  490. print('Could not parse BATCAVE\'s response as JSON for \'{0}\'->\'{1}\':'.format(nodeid, field, err))
  491. return None
  492. @willie.module.commands('debug-alfred')
  493. def ffpb_debug_alfred(bot, trigger):
  494. """Show statistics of available ALFRED data."""
  495. alfred_data = get_alfred_data(bot)
  496. if alfred_data is None:
  497. bot.say("Keine ALFRED-Daten vorhanden.")
  498. else:
  499. bot.say("ALFRED Daten: count={0} lastupdate={1}".format(len(alfred_data), bot.memory['alfred_update']))
  500. @willie.module.interval(60)
  501. def ffpb_updatepeers(bot):
  502. """Refresh list of peers and message the diff."""
  503. if peers_repo is None:
  504. print('WARNING: peers_repo is None')
  505. return
  506. old_head = peers_repo.head.commit
  507. peers_repo.remotes.origin.pull()
  508. new_head = peers_repo.head.commit
  509. if new_head != old_head:
  510. print('git pull: from ' + str(old_head) + ' to ' + str(new_head))
  511. added = []
  512. changed = []
  513. renamed = []
  514. deleted = []
  515. for f in old_head.diff(new_head):
  516. if f.new_file:
  517. added.append(f.b_blob.name)
  518. elif f.deleted_file:
  519. deleted.append(f.a_blob.name)
  520. elif f.renamed:
  521. renamed.append([f.rename_from, f.rename_to])
  522. else:
  523. changed.append(f.a_blob.name)
  524. response = "Knoten-Update (VPN +{0} %{1} -{2}): ".format(len(added), len(renamed)+len(changed), len(deleted))
  525. for f in added:
  526. response += " +'{}'".format(f)
  527. for f in changed:
  528. response += " %'{}'".format(f)
  529. for f in renamed:
  530. response += " '{}'->'{}'".format(f[0], f[1])
  531. for f in deleted:
  532. response += " -'{}'".format(f)
  533. bot.msg(bot.config.ffpb.msg_target, response)
  534. def ffpb_fetch_stats(bot, url, memoryid):
  535. """Fetch a ffmap-style nodes.json from the given URL and
  536. store it in the bot's memory."""
  537. response = urllib2.urlopen(url)
  538. data = json.load(response)
  539. nodes_active = 0
  540. nodes_total = 0
  541. clients_count = 0
  542. for node in data['nodes']:
  543. if node['flags']['gateway'] or node['flags']['client']:
  544. continue
  545. nodes_total += 1
  546. if node['flags']['online']:
  547. nodes_active += 1
  548. if 'legacy' in node['flags'] and node['flags']['legacy']:
  549. clients_count -= 1
  550. for link in data['links']:
  551. if link['type'] == 'client':
  552. clients_count += 1
  553. if not memoryid in bot.memory:
  554. bot.memory[memoryid] = {}
  555. stats = bot.memory[memoryid]
  556. stats["fetchtime"] = time.time()
  557. stats["nodes_active"] = nodes_active
  558. stats["nodes_total"] = nodes_total
  559. stats["clients"] = clients_count
  560. return (nodes_active, nodes_total, clients_count)
  561. def pretty_date(timestamp=False):
  562. """
  563. Get a datetime object or a int() Epoch timestamp and return a
  564. pretty string like 'an hour ago', 'Yesterday', '3 months ago',
  565. 'just now', etc
  566. """
  567. now = datetime.now()
  568. compare = None
  569. if type(timestamp) is int:
  570. compare = datetime.fromtimestamp(timestamp)
  571. elif type(timestamp) is float:
  572. compare = datetime.fromtimestamp(int(timestamp))
  573. elif isinstance(timestamp, datetime):
  574. compare = timestamp
  575. elif not timestamp:
  576. compare = now
  577. diff = now - compare
  578. second_diff = diff.seconds
  579. day_diff = diff.days
  580. if day_diff < 0:
  581. return ''
  582. if day_diff == 0:
  583. if second_diff < 10:
  584. return "gerade eben"
  585. if second_diff < 60:
  586. return "vor " + str(second_diff) + " Sekunden"
  587. if second_diff < 120:
  588. return "vor einer Minute"
  589. if second_diff < 3600:
  590. return "vor " + str(second_diff / 60) + " Minuten"
  591. if second_diff < 7200:
  592. return "vor einer Stunde"
  593. if second_diff < 86400:
  594. return "vor " + str(second_diff / 3600) + " Stunden"
  595. if day_diff == 1:
  596. return "gestern"
  597. if day_diff < 7:
  598. return "vor " + str(day_diff) + " Tagen"
  599. return "am " + compare.strftime('%d.%m.%Y um %H:%M Uhr')
  600. @willie.module.commands('ping')
  601. def ffpb_ping(bot, trigger=None, target_name=None, reply_directly=True):
  602. """Ping the given node"""
  603. # identify node or bail out
  604. if target_name is None:
  605. target_name = trigger.group(2)
  606. node = ffpb_findnode_from_botparam(bot, target_name,
  607. ensure_recent_alfreddata=False)
  608. if node is None:
  609. return None
  610. # get the first non-linklocal address from the node
  611. target = [x for x in node["network"]["addresses"] if not x.lower().startswith("fe80:")][0]
  612. target_alias = node["hostname"]
  613. # execute the actual ping and reply the result
  614. print("pinging '{0}' at {1} ...".format(target_name, target))
  615. result = os.system('ping6 -c 2 -W 1 ' + target + ' >/dev/null')
  616. if result == 0:
  617. print("ping to '{0}' succeeded".format(target_name))
  618. if reply_directly:
  619. bot.say('Knoten "' + target_alias + '" antwortet \\o/')
  620. return True
  621. elif result == 1 or result == 256:
  622. print("ping to '{0}' failed".format(target_name))
  623. if reply_directly:
  624. bot.say('Keine Antwort von "' + target_alias + '" :-(')
  625. return False
  626. else:
  627. print("ping to '{0}' broken: result='{1}'".format(target_name, result))
  628. if reply_directly:
  629. bot.say('Uh oh, irgendwas ist kaputt. Chef, ping result = ' + str(result) + ' - darf ich das essen?')
  630. return None
  631. @willie.module.commands('mesh')
  632. def ffpb_nodemesh(bot, trigger):
  633. """Display mesh partners of the given node."""
  634. # identify node or bail out
  635. target_name = trigger.group(2)
  636. node = ffpb_findnode_from_botparam(bot, target_name,
  637. ensure_recent_alfreddata=False)
  638. if node is None:
  639. return None
  640. # derive node's id
  641. nodeid = node['node_id'] if 'node_id' in node else None
  642. if nodeid is None:
  643. msg = 'Mist, ich habe gerade den Zettel verlegt auf dem die Node-ID von \'{0}\' steht, bitte frag später noch einmal.'
  644. bot.say(msg.format(node['hostname'] if 'hostname' in node else target_name))
  645. return
  646. # query BATCAVE for node's neighbours (result is a list of MAC addresses)
  647. cave_result = ffpb_get_batcave_nodefield(nodeid, 'neighbours')
  648. # query BATCAVE for neighbour's names
  649. data = '&'.join([str(n) for n in cave_result])
  650. req = urllib2.urlopen('http://[fdca:ffee:ff12:a255::253]:8888/idmac2name', data)
  651. # filter out duplicate names
  652. neighbours = set()
  653. for line in req:
  654. ident, name = line.strip().split('=')
  655. neighbours.add(name)
  656. neighbours = [x for x in neighbours]
  657. # respond to the user
  658. if len(neighbours) == 0:
  659. bot.say(u'{0} hat keinen Mesh-Partner *schnüff*'.format(node['hostname']))
  660. elif len(neighbours) == 1:
  661. bot.say(u'{0} mesht mit \'{1}\''.format(node['hostname'], neighbours[0]))
  662. else:
  663. bot.say('{0} mesht mit \'{1}\' und \'{2}\''.format(node['hostname'], '\', \''.join(neighbours[:-1]), neighbours[-1]))
  664. @willie.module.commands('exec-on-peer')
  665. def ffpb_remoteexec(bot, trigger):
  666. """Remote execution on the given node"""
  667. bot_params = trigger.group(2).split(' ', 1) if trigger.group(2) is not None else []
  668. if len(bot_params) != 2:
  669. bot.say('Wenn du nicht sagst wo mach ich remote execution bei dir!')
  670. bot.say('Tipp: !exec-on-peer <peer> <cmd>')
  671. return
  672. target_name = bot_params[0]
  673. target_cmd = bot_params[1]
  674. # identify requested node or bail out
  675. node = ffpb_findnode_from_botparam(bot, target_name,
  676. ensure_recent_alfreddata=False)
  677. if node is None:
  678. return
  679. # check ACL
  680. if not playitsafe(bot, trigger, via_channel=True, node=node):
  681. return
  682. # use the node's first non-linklocal address
  683. naddrs = node["network"]["addresses"]
  684. target = [x for x in naddrs if not x.lower().startswith("fe80:")][0]
  685. target_alias = node["hostname"]
  686. # assemble SSH command
  687. cmd = [
  688. 'ssh',
  689. '-6n',
  690. '-l', 'root',
  691. '-o', 'BatchMode=yes',
  692. '-o', 'StrictHostKeyChecking=no',
  693. target,
  694. target_cmd,
  695. ]
  696. print("REMOTE EXEC = " + str(cmd))
  697. try:
  698. # call SSH
  699. result = subprocess.check_output(
  700. cmd,
  701. stderr=subprocess.STDOUT,
  702. shell=False,
  703. )
  704. # fetch results and sent at most 8 of them as response
  705. lines = str(result).splitlines()
  706. if len(lines) == 0:
  707. bot.say('exec-on-peer(' + target_alias + '): No output')
  708. return
  709. msg = 'exec-on-peer({0}): {1} Zeilen'.format(target_alias, len(lines))
  710. if len(lines) > 8:
  711. msg += ' (zeige max. 8)'
  712. bot.say(msg + ':')
  713. for line in lines[0:8]:
  714. bot.say(line)
  715. except subprocess.CalledProcessError as err:
  716. bot.say('Fehler {0} bei exec-on-peer({1}): {2}'.format(
  717. err.returncode,
  718. target_alias,
  719. err.output
  720. ))