diff --git a/bencode.py b/bencode.py new file mode 100644 index 0000000..da4d035 --- /dev/null +++ b/bencode.py @@ -0,0 +1,309 @@ +# Written by Petru Paler +# see LICENSE.txt for license information +# http://cvs.degreez.net/viewcvs.cgi/*checkout*/bittornado/LICENSE.txt?rev=1.2 +# "the MIT license" + +def decode_int(x, f): + f += 1 + newf = x.index('e', f) + try: + n = int(x[f:newf]) + except (OverflowError, ValueError): + n = long(x[f:newf]) + if x[f] == '-': + if x[f + 1] == '0': + raise ValueError + elif x[f] == '0' and newf != f+1: + raise ValueError + return (n, newf+1) + +def decode_string(x, f): + colon = x.index(':', f) + try: + n = int(x[f:colon]) + except (OverflowError, ValueError): + n = long(x[f:colon]) + if x[f] == '0' and colon != f+1: + raise ValueError + colon += 1 + return (x[colon:colon+n], colon+n) + +def decode_list(x, f): + r, f = [], f+1 + while x[f] != 'e': + v, f = decode_func[x[f]](x, f) + r.append(v) + return (r, f + 1) + +def decode_dict(x, f): + r, f = {}, f+1 + lastkey = None + while x[f] != 'e': + k, f = decode_string(x, f) + if lastkey >= k: + raise ValueError + lastkey = k + r[k], f = decode_func[x[f]](x, f) + return (r, f + 1) + +decode_func = {} +decode_func['l'] = decode_list +decode_func['d'] = decode_dict +decode_func['i'] = decode_int +decode_func['0'] = decode_string +decode_func['1'] = decode_string +decode_func['2'] = decode_string +decode_func['3'] = decode_string +decode_func['4'] = decode_string +decode_func['5'] = decode_string +decode_func['6'] = decode_string +decode_func['7'] = decode_string +decode_func['8'] = decode_string +decode_func['9'] = decode_string + +def bdecode(x): + try: + r, l = decode_func[x[0]](x, 0) + except (IndexError, KeyError): + raise ValueError + if l != len(x): + raise ValueError + return r + +def test_bdecode(): + try: + bdecode('0:0:') + assert 0 + except ValueError: + pass + try: + bdecode('ie') + assert 0 + except ValueError: + pass + try: + bdecode('i341foo382e') + assert 0 + except ValueError: + pass + assert bdecode('i4e') == 4L + assert bdecode('i0e') == 0L + assert bdecode('i123456789e') == 123456789L + assert bdecode('i-10e') == -10L + try: + bdecode('i-0e') + assert 0 + except ValueError: + pass + try: + bdecode('i123') + assert 0 + except ValueError: + pass + try: + bdecode('') + assert 0 + except ValueError: + pass + try: + bdecode('i6easd') + assert 0 + except ValueError: + pass + try: + bdecode('35208734823ljdahflajhdf') + assert 0 + except ValueError: + pass + try: + bdecode('2:abfdjslhfld') + assert 0 + except ValueError: + pass + assert bdecode('0:') == '' + assert bdecode('3:abc') == 'abc' + assert bdecode('10:1234567890') == '1234567890' + try: + bdecode('02:xy') + assert 0 + except ValueError: + pass + try: + bdecode('l') + assert 0 + except ValueError: + pass + assert bdecode('le') == [] + try: + bdecode('leanfdldjfh') + assert 0 + except ValueError: + pass + assert bdecode('l0:0:0:e') == ['', '', ''] + try: + bdecode('relwjhrlewjh') + assert 0 + except ValueError: + pass + assert bdecode('li1ei2ei3ee') == [1, 2, 3] + assert bdecode('l3:asd2:xye') == ['asd', 'xy'] + assert bdecode('ll5:Alice3:Bobeli2ei3eee') == [['Alice', 'Bob'], [2, 3]] + try: + bdecode('d') + assert 0 + except ValueError: + pass + try: + bdecode('defoobar') + assert 0 + except ValueError: + pass + assert bdecode('de') == {} + assert bdecode('d3:agei25e4:eyes4:bluee') == {'age': 25, 'eyes': 'blue'} + assert bdecode('d8:spam.mp3d6:author5:Alice6:lengthi100000eee') == {'spam.mp3': {'author': 'Alice', 'length': 100000}} + try: + bdecode('d3:fooe') + assert 0 + except ValueError: + pass + try: + bdecode('di1e0:e') + assert 0 + except ValueError: + pass + try: + bdecode('d1:b0:1:a0:e') + assert 0 + except ValueError: + pass + try: + bdecode('d1:a0:1:a0:e') + assert 0 + except ValueError: + pass + try: + bdecode('i03e') + assert 0 + except ValueError: + pass + try: + bdecode('l01:ae') + assert 0 + except ValueError: + pass + try: + bdecode('9999:x') + assert 0 + except ValueError: + pass + try: + bdecode('l0:') + assert 0 + except ValueError: + pass + try: + bdecode('d0:0:') + assert 0 + except ValueError: + pass + try: + bdecode('d0:') + assert 0 + except ValueError: + pass + try: + bdecode('00:') + assert 0 + except ValueError: + pass + try: + bdecode('l-3:e') + assert 0 + except ValueError: + pass + try: + bdecode('i-03e') + assert 0 + except ValueError: + pass + bdecode('d0:i3ee') + +from types import StringType, IntType, LongType, DictType, ListType, TupleType + +class Bencached(object): + __slots__ = ['bencoded'] + + def __init__(self, s): + self.bencoded = s + +def encode_bencached(x,r): + r.append(x.bencoded) + +def encode_int(x, r): + r.extend(('i', str(x), 'e')) + +def encode_string(x, r): + r.extend((str(len(x)), ':', x)) + +def encode_list(x, r): + r.append('l') + for i in x: + encode_func[type(i)](i, r) + r.append('e') + +def encode_dict(x,r): + r.append('d') + ilist = x.items() + ilist.sort() + for k, v in ilist: + r.extend((str(len(k)), ':', k)) + encode_func[type(v)](v, r) + r.append('e') + +encode_func = {} +encode_func[type(Bencached(0))] = encode_bencached +encode_func[IntType] = encode_int +encode_func[LongType] = encode_int +encode_func[StringType] = encode_string +encode_func[ListType] = encode_list +encode_func[TupleType] = encode_list +encode_func[DictType] = encode_dict + +try: + from types import BooleanType + encode_func[BooleanType] = encode_int +except ImportError: + pass + +def bencode(x): + r = [] + encode_func[type(x)](x, r) + return ''.join(r) + +def test_bencode(): + assert bencode(4) == 'i4e' + assert bencode(0) == 'i0e' + assert bencode(-10) == 'i-10e' + assert bencode(12345678901234567890L) == 'i12345678901234567890e' + assert bencode('') == '0:' + assert bencode('abc') == '3:abc' + assert bencode('1234567890') == '10:1234567890' + assert bencode([]) == 'le' + assert bencode([1, 2, 3]) == 'li1ei2ei3ee' + assert bencode([['Alice', 'Bob'], [2, 3]]) == 'll5:Alice3:Bobeli2ei3eee' + assert bencode({}) == 'de' + assert bencode({'age': 25, 'eyes': 'blue'}) == 'd3:agei25e4:eyes4:bluee' + assert bencode({'spam.mp3': {'author': 'Alice', 'length': 100000}}) == 'd8:spam.mp3d6:author5:Alice6:lengthi100000eee' + assert bencode(Bencached(bencode(3))) == 'i3e' + try: + bencode({1: 'foo'}) + except TypeError: + return + assert 0 + +try: + import psyco + psyco.bind(bdecode) + psyco.bind(bencode) +except ImportError: + pass diff --git a/cjdns.py b/cjdns.py new file mode 100644 index 0000000..48edc7e --- /dev/null +++ b/cjdns.py @@ -0,0 +1,220 @@ +# You may redistribute this program and/or modify it under the terms of +# the GNU General Public License as published by the Free Software Foundation, +# either version 3 of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import sys; +import os; +import socket; +import hashlib; +import json; +import threading; +import time; +import Queue; +import random; +import string; +from bencode import *; + +BUFFER_SIZE = 69632; +KEEPALIVE_INTERVAL_SECONDS = 2; + +def randStr(): + return ''.join(random.choice(string.ascii_uppercase + string.digits) for x in range(10)); + +def callfunc(cjdns, funcName, password, args): + txid = randStr(); + sock = cjdns.socket; + sock.send('d1:q6:cookie4:txid10:'+ txid +'e'); + msg = _getMessage(cjdns, txid); + cookie = msg['cookie']; + txid = randStr(); + req = { + 'q': 'auth', + 'aq': funcName, + 'hash': hashlib.sha256(password + cookie).hexdigest(), + 'cookie' : cookie, + 'args': args, + 'txid': txid + }; + reqBenc = bencode(req); + req['hash'] = hashlib.sha256(reqBenc).hexdigest(); + reqBenc = bencode(req); + sock.send(reqBenc); + return _getMessage(cjdns, txid); + + +def receiverThread(cjdns): + timeOfLastSend = time.time(); + timeOfLastRecv = time.time(); + try: + while True: + if (timeOfLastSend + KEEPALIVE_INTERVAL_SECONDS < time.time()): + if (timeOfLastRecv + 10 < time.time()): + raise Exception("ping timeout"); + cjdns.socket.send('d1:q18:Admin_asyncEnabled4:txid8:keepalive'); + timeOfLastSend = time.time(); + + try: + data = cjdns.socket.recv(BUFFER_SIZE); + except (socket.timeout): continue; + + try: + benc = bdecode(data); + except (KeyError, ValueError): + print("error decoding [" + data + "]"); + continue; + + if benc['txid'] == 'keepaliv': + if benc['asyncEnabled'] == 0: + raise Exception("lost session"); + timeOfLastRecv = time.time(); + else: + #print "putting to queue " + str(benc); + cjdns.queue.put(benc); + + except KeyboardInterrupt: + print("interrupted"); + import thread + thread.interrupt_main(); + + +def _getMessage(cjdns, txid): + while True: + if txid in cjdns.messages: + msg = cjdns.messages[txid]; + del cjdns.messages[txid]; + return msg; + else: + #print "getting from queue"; + try: + # apparently any timeout at all allows the thread to be + # stopped but none make it unstoppable with ctrl+c + next = cjdns.queue.get(timeout=100); + except Queue.Empty: continue; + if 'txid' in next: + cjdns.messages[next['txid']] = next; + #print "adding message [" + str(next) + "]"; + else: + print "message with no txid: " + str(next); + + +def cjdns_connect(ipAddr, port, password): + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM); + sock.connect((ipAddr, port)); + sock.settimeout(2); + + # Make sure it pongs. + sock.send('d1:q4:pinge'); + data = sock.recv(BUFFER_SIZE); + if (data != 'd1:q4:ponge'): + raise Exception("Looks like " + ipAddr + ":" + str(port) + " is to a non-cjdns socket."); + + # Get the functions and make the object + page = 0; + availableFunctions = {}; + while True: + sock.send('d1:q24:Admin_availableFunctions4:argsd4:pagei' + str(page) + 'eee'); + data = sock.recv(BUFFER_SIZE); + benc = bdecode(data); + for func in benc['availableFunctions']: + availableFunctions[func] = benc['availableFunctions'][func]; + if (not 'more' in benc): + break; + page = page+1; + + argLists = {}; + cc = ("class Cjdns:\n" + + " def __init__(self, socket):\n" + + " self.socket = socket;\n" + + " self.queue = Queue.Queue();\n" + + " self.messages = {};\n" + + " def disconnect(self):\n" + + " self.socket.close();\n" + + " def getMessage(self, txid):\n" + + " return _getMessage(self, txid);\n"); + + for func in availableFunctions: + argList = []; + argLists[func] = argList; + funcDict = availableFunctions[func]; + cc += " def " + func + "(self"; + + # If the arg is required, put it first, + # otherwise put it after and use a default + # value of a type which will be ignored by the core. + for arg in funcDict: + if (funcDict[arg]['required'] == 1): + argList.append(arg); + cc += ", " + arg; + for arg in funcDict: + argDict = funcDict[arg]; + if (argDict['required'] == 0): + argList.append(arg); + cc += ", " + arg + "="; + if (argDict['type'] == 'Int'): + cc += "''"; + else: + cc += "0"; + + cc += ("):\n" + + " args = {"); + for arg in argList: + cc += "\"" + arg + "\": " + arg + ", "; + cc += ("};\n" + + " return callfunc(self, \"" + func + "\", \"" + password + "\", args);\n"); + exec(cc); + + cjdns = Cjdns(sock); + + kat = threading.Thread(target=receiverThread, args=[cjdns]); + kat.setDaemon(True); + kat.start(); + + # Check our password. + ret = callfunc(cjdns, "ping", password, {}); + if ('error' in ret): + raise Exception("Connect failed, incorrect admin password?\n" + str(ret)) + + cjdns.functions = ""; + nl = ""; + for func in availableFunctions: + funcDict = availableFunctions[func]; + cjdns.functions += nl + func + "("; + nl = "\n"; + sep = ""; + for arg in argLists[func]: + cjdns.functions += sep; + sep = ", "; + argDict = funcDict[arg]; + if (argDict['required'] == 1): + cjdns.functions += "required "; + cjdns.functions += argDict['type'] + " " + arg; + cjdns.functions += ")"; + + return cjdns; + + +def cjdns_connectWithAdminInfo(): + try: + adminInfo = open(os.getenv("HOME") + '/.cjdnsadmin', 'r'); + except IOError: + print('Please create a file named .cjdnsadmin in your home directory with'); + print('ip, port, and password of your cjdns engine in json.'); + print('for example:'); + print('{'); + print(' "addr": "127.0.0.1",'); + print(' "port": 11234,'); + print(' "password": "You tell me! (you\'ll find it in your cjdroute.conf)"'); + print('}'); + raise; + + data = json.load(adminInfo); + adminInfo.close(); + return cjdns_connect(data['addr'], data['port'], data['password']); diff --git a/cjdnsmap.py b/cjdnsmap.py index f6a8ee1..5d61d33 100755 --- a/cjdnsmap.py +++ b/cjdnsmap.py @@ -18,12 +18,54 @@ # - Color nodes depending on the number of connections # -import pydot +######## SETTINGS ############### +credentialsFromConfig = True # Whether or not to get cjdns credentials from ~/.cjdnsadmin +nonames = False # Should we even bother trying to look up names? +cjdadmin_ip = "127.0.0.1" # +cjdadmin_port = 11234 # +cjdadmin_pass = "insecure_pass" # +filename = 'map.svg' # Picks format based on filename. If it ends in .svg it's an svg, otherwise it's a png +################################# + import re import socket -import httplib2 import sys import math +import json +try: + import httplib2 +except: + print "Requires httplib2, try: " + print "sudo easy_install httplib2" + sys.exit() +try: + import pydot +except: + print "Requires pydot, try:" + print "sudo easy_install pydot" + sys.exit() +try: + from cjdns import cjdns_connect, cjdns_connectWithAdminInfo +except: + print "Requires cjdns python module. It should've been included" + print "with this program. Please ensure that it's in the path. It" + print "also comes with cjdns, check contrib/python/ in the cjdns source" + sys.exit() + + +if len(sys.argv) == 5: + cjdadmin_ip = sys.argv[1] + cjdadmin_port = int(sys.argv[2]) + cjdadmin_pass = sys.argv[3] + filename = sys.argv[4] + print "Using credentials from argv" +elif len(sys.argv) == 2: + filename = sys.argv[1] +elif len(sys.argv) != 1: + print "Usage is:" + print sys.argv[0] + " " + print "Or:" + print sys.argv[0] + " []" ################################################# # code from http://effbot.org/zone/bencode.htm @@ -123,7 +165,11 @@ def hsv_to_color(h,s,v): ################################################### - +try: + cjdns = cjdns_connect(cjdadmin_ip, cjdadmin_port, cjdadmin_pass) +except: + cjdns = cjdns_connectWithAdminInfo() + class route: def __init__(self, ip, name, path, link): self.ip = ip @@ -149,7 +195,7 @@ def __init__(self, ip, name, path, link): route = route.replace('y','0001') route = route.replace('x','0000') self.route = route[::-1].rstrip('0')[:-1] - self.quality = link / 536870.0 # LINK_STATE_MULTIPLIER + self.quality = link / 5366870.0 # LINK_STATE_MULTIPLIER def find_parent(self, routes): parents = [(len(other.route),other) for other in routes if self.route.startswith(other.route) and self != other] @@ -160,13 +206,32 @@ def find_parent(self, routes): return parent return None -if len(sys.argv) > 1: - filename = sys.argv[-1] +# retrieve the node names from the page maintained by Mikey +page = 'http://[fc5d:baa5:61fc:6ffd:9554:67f0:e290:7535]/nodes/list.json' +print('Downloading the list of node names from {0} ...'.format(page)) +names = {} +h = httplib2.Http(".cache") +if not nonames: + try: + r, content = h.request(page, "GET") + nameip = json.loads(content)['nodes'] + except: + print "Connection to Mikey's nodelist failed, continuing without names" + nameip = {} else: - filename = 'map.png' - -# retrieve the node names from the page maintained by ircerr -page = 'http://[fc38:4c2c:1a8f:3981:f2e7:c2b9:6870:6e84]/ipv6-cjdnet.data.txt' + nameip = {} + +existing_names = set() +doubles = set() + +for node in nameip: + if not node['name'] in doubles: + names[node['ip']]=node['name'] + else: + names[node['ip']]=node['name'] + ' ' + ip.split(':')[-1] + +""" +page = 'http://ircerr.bt-chat.com/cjdns/ipv6-cjdnet.data.txt' print('Downloading the list of node names from {0} ...'.format(page)) names = {} h = httplib2.Http(".cache") @@ -194,37 +259,21 @@ def find_parent(self, routes): names[ip]=name else: names[ip]=name + ' ' + ip.split(':')[-1] +""" -# retrieve the routing data from the admin interface -# FIXME: read these from the commandline or even from the config -HOST = 'localhost' -PORT = 11234 -print('Retrieving the routing table from the admin interface at {0} port {1}'.format(HOST,PORT)) -s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) -s.connect((HOST, PORT)) -s.send('d1:q19:NodeStore_dumpTable4:txid4:....e') -data = '' +routes = []; +i = 0; while True: - r = s.recv(1024) - data += r - if not r or r.endswith('....e\n'): + table = cjdns.NodeStore_dumpTable(i) + for r in table['routingTable']: + name = r['ip'].split(':')[-1] + if r['ip'] in names: + name = names[r['ip']] + routes.append(route(r['ip'],name,r['path'],r['link'])) + if not 'more' in table: break -s.shutdown(socket.SHUT_RDWR) -s.close() -data = data.strip() -bencode = decode(data) + i += 1 -routes = [] -for r in bencode['routingTable']: - ip = r['ip'] - path = r['path'] - link = r['link'] - if ip in names: - name = names[ip] - else: - name = ip.split(':')[-1] - r = route(ip,name,path,link) - routes.append(r) # sort the routes on quality tmp = [(r.quality,r) for r in routes] @@ -329,7 +378,7 @@ def add_edges(active,color): add_edges(True,'black') add_edges(False,'grey') -graph = pydot.Dot(graph_type='graph', K='2', splines='true', dpi='50', maxiter='10000', ranksep='2', nodesep='1', epsilon='0.1', overlap='true') +graph = pydot.Dot(graph_type='graph', K='2', splines='true', dpi='50', maxiter='10000', ranksep='2', nodesep='1', epsilon='0.1', overlap='false') calculate_family_hues() for n in nodes.itervalues(): graph.add_node(n.Node()) @@ -348,5 +397,8 @@ def add_edges(active,color): graph.add_edge(edge) print('Generating the map...') -graph.write_png(filename, prog='fdp') # dot neato twopi fdp circo +if filename.split(".")[-1] == "svg": + graph.write_svg(filename, prog='fdp') +else: + graph.write_png(filename, prog='fdp') print('Map written to {0}'.format(filename))