Browse Source

Added a python3 library and an example of signing a message

Caleb James DeLisle 3 years ago
parent
commit
b3348d1f47

+ 0 - 0
contrib/python3/cjdnsadmin/__init__.py


+ 170 - 0
contrib/python3/cjdnsadmin/adminTools.py

@@ -0,0 +1,170 @@
+#!/usr/bin/env python2
+# 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 <https://www.gnu.org/licenses/>.
+
+import os
+import json
+
+from time import sleep
+
+def anonConnect(ip='127.0.0.1', port=11234):
+    from cjdnsadmin import connect
+    path = os.path.expanduser('~/.cjdnsadmin')
+    try:
+        with open(path, 'r') as adminInfo:
+            data = json.load(adminInfo)
+        return connect(data['addr'], data['port'], '')
+    except IOError:
+        return connect(ip, int(port), '')
+
+def connect(ip='127.0.0.1', port=11234, password=''):
+    from cjdnsadmin import connectWithAdminInfo
+    return connectWithAdminInfo()
+
+def disconnect(cjdns):
+    cjdns.disconnect()
+
+def whoami(cjdns):
+    from publicToIp6 import PublicToIp6_convert;
+    resp=cjdns.NodeStore_nodeForAddr(0)
+    key=resp['result']['key']
+    ver=resp['result']['protocolVersion']
+    IP=PublicToIp6_convert(key)
+    return {'IP':IP,'key':key,'version':ver}
+
+def dumpTable(cjdns,verbose=False,unique_ip=False,nodes=[]):
+    if nodes == []: nodes=[]
+    rt = []
+    i = 0;
+    while True:
+        table = cjdns.NodeStore_dumpTable(i)
+        res=table['routingTable']
+        for t in res:
+            ip=t['ip']
+            if (not ip in nodes) and unique_ip:
+                nodes.append(ip)
+                rt.append(t)
+                if verbose:
+                    print(t['ip'] + ' ' + t['path'] + ' ' + str(t['link']) + ' ' + str(t['version']));
+            if not unique_ip:
+                nodes.append(ip)
+                rt.append(t)
+                if verbose:
+                    print(t['ip'] + ' ' + t['path'] + ' ' + str(t['link']) + ' ' + str(t['version']));
+        if not 'more' in table:
+            break
+        i += 1
+
+    return rt
+
+def streamRoutingTable(cjdns, delay=10):
+    known = []
+
+    while True:
+        i = 0
+        while True:
+            table = cjdns.NodeStore_dumpTable(i)
+            routes = table['routingTable']
+            for entry in routes:
+                if entry['ip'] not in known:
+                    known.append(entry['ip'])
+                    yield entry
+
+            if 'more' not in table:
+                break
+
+            i += 1
+
+        sleep(delay)
+
+def parseAddr(addr):
+    tokens = addr.split('.', 5)
+    res = {
+            'version': tokens[0].strip('v'),
+            'switchLabel': '.'.join(tokens[1:5]),
+            'publicKey': tokens[5],
+            }
+    return res
+
+def peerStats(cjdns,up=False,verbose=False,human_readable=False):
+    from publicToIp6 import PublicToIp6_convert;
+
+    allPeers = []
+
+    i = 0;
+    while True:
+        ps = cjdns.InterfaceController_peerStats(page=i);
+        peers = ps['peers']
+        for p in peers:
+            p.update(parseAddr(p['addr']))
+            if p['state'] == 'UNRESPONSIVE' and up:
+                continue
+            allPeers.append(p)
+        if (not 'more' in ps):
+            break
+        i += 1
+
+    if verbose:
+        STAT_FORMAT = '%s\t%s\tv%s\t%s\tin %s\tout %s\t%s\tdup %d los %d oor %d'
+
+        for peer in allPeers:
+            ip = PublicToIp6_convert(peer['publicKey'])
+			
+            b_in  = peer['bytesIn']
+            b_out = peer['bytesOut']
+            if human_readable:
+				b_in  = sizeof_fmt(b_in)
+				b_out = sizeof_fmt(b_out)
+            
+            p = STAT_FORMAT % (peer['lladdr'], ip, peer['version'], peer['switchLabel'],
+                               str(b_in), str(b_out), peer['state'],
+                               peer['duplicates'], peer['lostPackets'],
+                               peer['receivedOutOfRange'])
+
+            if 'user' in peer:
+                p += '\t%r' % peer['user']
+
+            print p
+    return allPeers
+
+def sizeof_fmt(num):
+    for x in ['B','KB','MB','GB','TB']:
+        if num < 1024.0:
+            return "%3.1f%s" % (num, x)
+        num /= 1024.0
+
+def parseLabel(route):
+    route = route.replace('.','')
+    broute= int('0x' + route, 16);
+    route = route.replace('0','x')
+    route = route.replace('1','y')
+    route = route.replace('f','1111')
+    route = route.replace('e','1110')
+    route = route.replace('d','1101')
+    route = route.replace('c','1100')
+    route = route.replace('b','1011')
+    route = route.replace('a','1010')
+    route = route.replace('9','1001')
+    route = route.replace('8','1000')
+    route = route.replace('7','0111')
+    route = route.replace('6','0110')
+    route = route.replace('5','0101')
+    route = route.replace('4','0100')
+    route = route.replace('3','0011')
+    route = route.replace('2','0010')
+    route = route.replace('y','0001')
+    route = route.replace('x','0000')
+    # reverse the string, strip trailing zeros, then strip the trailing 1
+    route = route[::-1].rstrip('0')[:-1]
+    return {'route':route,'broute':broute}
+
+

+ 309 - 0
contrib/python3/cjdnsadmin/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])
+# Leading zeros are FINE --cjd
+#    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 != None and 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_stream(x):
+    return decode_func[x[0]](x, 0);
+
+def bdecode(x):
+    try:
+        r, l = bdecode_stream(x);
+    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') == 4
+    assert bdecode('i0e') == 0
+    assert bdecode('i123456789e') == 123456789
+    assert bdecode('i-10e') == -10
+    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')
+
+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 = sorted(x.items())
+    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[type(0)] = encode_int
+encode_func[type('')] = encode_string
+encode_func[type([])] = encode_list
+encode_func[type(())] = encode_list
+encode_func[type({})] = 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(12345678901234567890) == '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

+ 25 - 0
contrib/python3/cjdnsadmin/bencode.py.LICENSE.txt

@@ -0,0 +1,25 @@
+Unless otherwise noted, all files are released under the MIT
+license, exceptions contain licensing information in them.
+
+Copyright (C) 2001-2004 Bram Cohen
+Copyright (C) 2003-2007 John Hoffman
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation files
+(the "Software"), to deal in the Software without restriction,
+including without limitation the rights to use, copy, modify, merge,
+publish, distribute, sublicense, and/or sell copies of the Software,
+and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+The Software is provided "AS IS", without warranty of any kind,
+express or implied, including but not limited to the warranties of
+merchantability,  fitness for a particular purpose and
+noninfringement. In no event shall the  authors or copyright holders
+be liable for any claim, damages or other liability, whether in an
+action of contract, tort or otherwise, arising from, out of or in
+connection with the Software or the use or other dealings in the
+Software.

+ 320 - 0
contrib/python3/cjdnsadmin/cjdnsadmin.py

@@ -0,0 +1,320 @@
+# 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 <https://www.gnu.org/licenses/>.
+
+import sys
+import os
+import socket
+import errno
+import hashlib
+import json
+import threading
+import time
+import queue
+import random
+import string
+from .bencode import *
+
+BUFFER_SIZE = 69632
+KEEPALIVE_INTERVAL_SECONDS = 2
+
+
+class Session():
+    """Current cjdns admin session"""
+
+    def __init__(self, socket):
+        self.socket = socket
+        self.queue = queue.Queue()
+        self.messages = {}
+
+    def disconnect(self):
+        self.socket.close()
+
+    def getMessage(self, txid):
+        # print self, txid
+        return _getMessage(self, txid)
+
+    def functions(self):
+        print(self._functions)
+
+
+def _randomString():
+    """Random string for message signing"""
+
+    return ''.join(
+        random.choice(string.ascii_uppercase + string.digits)
+        for x in range(10))
+
+
+def _callFunc(session, funcName, password, args):
+    """Call custom cjdns admin function"""
+
+    txid = _randomString()
+    sock = session.socket
+    sock.send(('d1:q6:cookie4:txid10:' + txid + 'e').encode('utf-8'))
+    msg = _getMessage(session, txid)
+    cookie = msg['cookie']
+    txid = _randomString()
+    req = {
+        'q': funcName,
+        'hash': hashlib.sha256((password + cookie).encode('utf-8')).hexdigest(),
+        'cookie': cookie,
+        'args': args,
+        'txid': txid
+    }
+
+    if password:
+        req['aq'] = req['q']
+        req['q'] = 'auth'
+        reqBenc = bencode(req)
+        req['hash'] = hashlib.sha256(reqBenc.encode('utf-8')).hexdigest()
+
+    reqBenc = bencode(req)
+    sock.send(reqBenc.encode('utf-8'))
+    return _getMessage(session, txid)
+
+
+def _receiverThread(session):
+    """Receiving messages from cjdns admin server"""
+
+    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")
+                session.socket.send(
+                    b'd1:q18:Admin_asyncEnabled4:txid8:keepalive')
+                timeOfLastSend = time.time()
+
+            # Did we get data from the socket?
+            got_data = False
+
+            while True:
+                # This can be interrupted and we need to loop it.
+
+                try:
+                    data = session.socket.recv(BUFFER_SIZE).decode('utf-8')
+                except (socket.timeout):
+                    # Stop retrying, but note we have no data
+                    break
+                except socket.error as e:
+                    if e.errno != errno.EINTR:
+                        # Forward errors that aren't being interrupted
+                        raise
+                    # Otherwise it was interrupted so we try again.
+                else:
+                    # Don't try again, we got data
+                    got_data = True
+                    break
+
+            if not got_data:
+                # Try asking again.
+                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)
+                session.queue.put(benc)
+
+    except KeyboardInterrupt:
+        print("interrupted")
+        import thread
+        thread.interrupt_main()
+    except Exception as e:
+        # Forward along any errors, before killing the thread.
+        session.queue.put(e)
+
+
+def _getMessage(session, txid):
+    """Getting message associated with txid"""
+
+    while True:
+        if txid in session.messages:
+            msg = session.messages[txid]
+            del session.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 = session.queue.get(timeout=100)
+            except Queue.Empty:
+                continue
+
+            if isinstance(next, Exception):
+                # If the receiveing thread had an error, throw one here too.
+                raise next
+
+            if 'txid' in next:
+                session.messages[next['txid']] = next
+                # print "adding message [" + str(next) + "]"
+            else:
+                print("message with no txid: " + str(next))
+
+
+def _functionFabric(func_name, argList, oargs, oargNames, password):
+    """Function fabric for Session class"""
+
+    def functionHandler(self, *args, **kwargs):
+        call_args = {}
+        
+        pos = 0
+        for value in args:
+            if (pos < len(argList)):
+                call_args[argList[pos]] = value
+                pos += 1
+            elif (pos < len(argList) + len(oargNames)):
+                call_args[oargNames[pos - len(argList)]] = value
+            else:
+                print("warning: extraneous argument passed to function",func_name,value)
+
+        for (key, value) in kwargs.items():
+            if key not in oargs:
+                if key in argList:
+                    # this is a positional argument, given a keyword name
+                    # that happens in python.
+                    # TODO: we can't handle this along with unnamed positional args.
+                    pos = argList.index(key)
+                    call_args[argList[pos]] = value
+                    continue
+                else:
+                    print("warning: not an argument to this function",func_name,key)
+                    print(oargs)
+            else:
+                # TODO: check oargs[key] type matches value
+                # warn, if doesn't
+                call_args[key] = value
+
+        return _callFunc(self, func_name, password, call_args)
+
+    functionHandler.__name__ = func_name
+    return functionHandler
+
+
+def connect(ipAddr, port, password):
+    """Connect to cjdns admin with this attributes"""
+
+    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+    sock.connect((ipAddr, port))
+    sock.settimeout(2)
+
+    # Make sure it pongs.
+    sock.send(b'd1:q4:pinge')
+    data = sock.recv(BUFFER_SIZE).decode('utf-8')
+    if (not data.endswith('1: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').encode('utf-8'))
+        data = sock.recv(BUFFER_SIZE).decode('utf-8')
+        benc = bdecode(data)
+        for func in benc['availableFunctions']:
+            availableFunctions[func] = benc['availableFunctions'][func]
+        if (not 'more' in benc):
+            break
+        page = page+1
+
+    funcArgs = {}
+    funcOargs = {}
+
+    for (i, func) in availableFunctions.items():
+        items = func.items()
+
+        # required args
+        argList = []
+        # optional args
+        oargs = {}
+        # order of optional args for python-style calling
+        oargNames = []
+        
+        for (arg,atts) in items:
+            if atts['required']:
+                argList.append(arg)
+            else:
+                oargs[arg] = atts['type']
+                oargNames.append(arg)
+
+        setattr(Session, i, _functionFabric(
+            i, argList, oargs, oargNames, password))
+
+        funcArgs[i] = argList
+        funcOargs[i] = oargs
+
+    session = Session(sock)
+
+    kat = threading.Thread(target=_receiverThread, args=[session])
+    kat.setDaemon(True)
+    kat.start()
+
+    # Check our password.
+    ret = _callFunc(session, "ping", password, {})
+    if ('error' in ret):
+        raise Exception(
+            "Connect failed, incorrect admin password?\n" + str(ret))
+
+    session._functions = ""
+
+    funcOargs_c = {}
+    for func in funcOargs:
+        funcOargs_c[func] = list(
+            [key + "=" + str(value)
+                for (key, value) in funcOargs[func].items()])
+
+    for func in availableFunctions:
+        session._functions += (
+            func + "(" + ', '.join(funcArgs[func] + funcOargs_c[func]) + ")\n")
+
+    # print session.functions
+    return session
+
+
+def connectWithAdminInfo(path = None):
+    """Connect to cjdns admin with data from user file"""
+
+    if path is None:
+        path = os.path.expanduser('~/.cjdnsadmin')
+    try:
+        with open(path, 'r') as adminInfo:
+            data = json.load(adminInfo)
+    except IOError:
+        sys.stderr.write("""Please create a file named .cjdnsadmin in your
+home directory with
+ip, port, and password of your cjdns engine in json.
+for example:
+{
+    "addr": "127.0.0.1",
+    "port": 11234,
+    "password": "You tell me! (Search in ~/cjdroute.conf)"
+}
+""")
+        raise
+
+    return connect(data['addr'], data['port'], data['password'])

+ 70 - 0
contrib/python3/cjdnsadmin/cli.py

@@ -0,0 +1,70 @@
+#!/usr/bin/env python2
+# 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 <https://www.gnu.org/licenses/>.
+
+import sys
+import os
+import cjdnsadmin
+import json
+import getopt
+import string
+
+def usage():
+  """ print usage information """
+
+  print """
+Cjdns admin command line interface.
+Usage: [OPTION]... RPC
+  RPC              the function name w/ arguments of the RPC you want to make.
+
+options:
+  -c  --config=    the cjdnsadmin file to use.  Defaults to ~/.cjdnsadmin
+  -h, --help       display this help and exit
+  -p, --pretty     format the output of the RPC as formatted JSON
+
+Example:
+  'functions()'    Prints the list of functions available.
+"""
+
+
+def parse(args):
+  """ parse the command line arguments """
+
+  try:
+    return getopt.getopt(args, 'phc:', ['pretty','help','config='])
+  except getopt.GetoptError:
+    usage()
+    sys.exit(2)
+
+
+if __name__ == "__main__":
+
+  options, remainder = parse(sys.argv[1:])
+  transform = lambda s: s
+  connect = lambda : cjdnsadmin.connectWithAdminInfo()
+
+  for opt, arg in options:
+    if opt in ('-c', '--config'):
+      connect = lambda :  cjdnsadmin.connectWithAdminInfo(arg)
+    elif opt in ('-h', '--help'):
+      usage()
+      sys.exit(0)
+    elif opt in ('-p', '--pretty'):
+      transform = lambda s: json.dumps(s, sort_keys=True, indent=4, separators=(',', ': '))
+
+  if remainder:
+    s = connect()
+    result = eval('s.' + string.join(remainder," "))
+    if result:
+      print transform(result)
+  else:
+    usage()

+ 57 - 0
contrib/python3/cjdnsadmin/graphMaker.py

@@ -0,0 +1,57 @@
+#!/usr/bin/env python2
+# 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 <https://www.gnu.org/licenses/>.
+
+def makeGraph():
+    import adminTools as admin
+    import networkx as nx
+    from publicToIp6 import PublicToIp6_convert
+    from collections import deque
+
+    cjdns=admin.connect()
+    root=admin.whoami(cjdns)
+    rootIP=root['IP']
+
+    G=nx.Graph()
+    G.add_node(rootIP[-4:],ip=rootIP)
+
+    nodes=deque()
+    nodes.append(rootIP)
+    while len(nodes) != 0:
+        parentIP=nodes.popleft()
+        resp=cjdns.NodeStore_nodeForAddr(parentIP)
+        numLinks=0
+	if 'result' in resp:
+            link=resp['result']
+            if 'linkCount' in link:
+                numLinks=int(resp['result']['linkCount'])
+                G.node[parentIP[-4:]]['version']=resp['result']['protocolVersion']
+
+        for i in range(0,numLinks):
+            resp = cjdns.NodeStore_getLink(i, parent=parentIP)
+            childLink=resp['result']
+            if not childLink: continue
+            childAddr=admin.parseAddr(childLink['child'])
+            childIP=PublicToIp6_convert(childAddr['publicKey'])
+            # Check to see if its one hop away from parent node
+            if childLink['isOneHop'] != 1:
+                continue
+            # If its a new node then we want to follow it
+            if not childIP[-4:] in G.nodes():
+                G.add_node(childIP[-4:],ip=childIP)
+                G.node[childIP[-4:]]['version']=0
+                nodes.append(childIP)
+            # If there is not a link between the nodes we should put one there
+            if (not childIP[-4:] in G[parentIP[-4:]]):
+                G.add_edge(parentIP[-4:],childIP[-4:])
+
+    return G

+ 65 - 0
contrib/python3/cjdnsadmin/publicToIp6.py

@@ -0,0 +1,65 @@
+#!/usr/bin/env python2
+# 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 <https://www.gnu.org/licenses/>.
+
+from hashlib import sha512;
+
+# see util/Base32.h
+def Base32_decode(input):
+    output = bytearray(len(input));
+    numForAscii = [
+        99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,
+        99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,
+        99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,
+         0, 1, 2, 3, 4, 5, 6, 7, 8, 9,99,99,99,99,99,99,
+        99,99,10,11,12,99,13,14,15,99,16,17,18,19,20,99,
+        21,22,23,24,25,26,27,28,29,30,31,99,99,99,99,99,
+        99,99,10,11,12,99,13,14,15,99,16,17,18,19,20,99,
+        21,22,23,24,25,26,27,28,29,30,31,99,99,99,99,99,
+    ];
+
+    outputIndex = 0;
+    inputIndex = 0;
+    nextByte = 0;
+    bits = 0;
+
+    while (inputIndex < len(input)):
+        o = ord(input[inputIndex]);
+        if (o & 0x80): raise ValueError;
+        b = numForAscii[o];
+        inputIndex += 1;
+        if (b > 31): raise ValueError("bad character " + input[inputIndex]);
+
+        nextByte |= (b << bits);
+        bits += 5;
+
+        if (bits >= 8):
+            output[outputIndex] = nextByte & 0xff;
+            outputIndex += 1;
+            bits -= 8;
+            nextByte >>= 8;
+
+    if (bits >= 5 or nextByte):
+        raise ValueError("bits is " + str(bits) + " and nextByte is " + str(nextByte));
+
+    return buffer(output, 0, outputIndex);
+
+
+def PublicToIp6_convert(pubKey):
+    if pubKey[-2:] != ".k":
+        raise ValueError("key does not end with .k")
+
+    keyBytes = Base32_decode(pubKey[:-2])
+    hashOne = sha512(keyBytes).digest()
+    hashTwo = sha512(hashOne).hexdigest()
+
+    return ":".join([hashTwo[i:i+4] for i in range(0, 32, 4)])

+ 23 - 0
contrib/python3/cjdnsadmin/shell.py

@@ -0,0 +1,23 @@
+#!/usr/bin/env python2
+# 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 <https://www.gnu.org/licenses/>.
+
+import sys
+import os
+import cjdnsadmin
+
+s = cjdnsadmin.connectWithAdminInfo()
+
+print("Interactive cjdns admin shell.")
+print("Usage: `s.command()`. 's' for Session.")
+print("Try s.ping() or s.functions() to start.")
+print("Ctrl-D to exit.")

+ 5 - 0
contrib/python3/sign_example.py

@@ -0,0 +1,5 @@
+from cjdnsadmin.cjdnsadmin import connect
+cjdns = connect("127.0.0.1", 11234, "NONE")
+print(cjdns.Sign_checkSig(
+    'test message',
+    '0ytl2njc1hy86tlxtc2zc3449up47uqb0u04kcy233d7zrn2cwh1_y96duzwpvmslj8b7pnk2b32m0rhs738yujwtrtlcq81r0u114svygwn56phn9yncpyzhswpj3bd808lgd5bknlj8xwf7purl0r0hc30'))