# 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 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') msg = _getMessage(session, txid) cookie = msg['cookie'] txid = _randomString() req = { 'q': funcName, 'hash': hashlib.sha256(password + cookie).hexdigest(), 'cookie': cookie, 'args': args, 'txid': txid } if password: req['aq'] = req['q'] req['q'] = 'auth' reqBenc = bencode(req) req['hash'] = hashlib.sha256(reqBenc).hexdigest() reqBenc = bencode(req) sock.send(reqBenc) 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( '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) 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('d1:q4:pinge') data = sock.recv(BUFFER_SIZE) 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') 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 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'])