123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320 |
- # 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'])
|