|
@@ -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'])
|