cjdnsadmin.py 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320
  1. # You may redistribute this program and/or modify it under the terms of
  2. # the GNU General Public License as published by the Free Software Foundation,
  3. # either version 3 of the License, or (at your option) any later version.
  4. #
  5. # This program is distributed in the hope that it will be useful,
  6. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  7. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  8. # GNU General Public License for more details.
  9. #
  10. # You should have received a copy of the GNU General Public License
  11. # along with this program. If not, see <https://www.gnu.org/licenses/>.
  12. import sys
  13. import os
  14. import socket
  15. import errno
  16. import hashlib
  17. import json
  18. import threading
  19. import time
  20. import Queue
  21. import random
  22. import string
  23. from bencode import *
  24. BUFFER_SIZE = 69632
  25. KEEPALIVE_INTERVAL_SECONDS = 2
  26. class Session():
  27. """Current cjdns admin session"""
  28. def __init__(self, socket):
  29. self.socket = socket
  30. self.queue = Queue.Queue()
  31. self.messages = {}
  32. def disconnect(self):
  33. self.socket.close()
  34. def getMessage(self, txid):
  35. # print self, txid
  36. return _getMessage(self, txid)
  37. def functions(self):
  38. print(self._functions)
  39. def _randomString():
  40. """Random string for message signing"""
  41. return ''.join(
  42. random.choice(string.ascii_uppercase + string.digits)
  43. for x in range(10))
  44. def _callFunc(session, funcName, password, args):
  45. """Call custom cjdns admin function"""
  46. txid = _randomString()
  47. sock = session.socket
  48. sock.send('d1:q6:cookie4:txid10:' + txid + 'e')
  49. msg = _getMessage(session, txid)
  50. cookie = msg['cookie']
  51. txid = _randomString()
  52. req = {
  53. 'q': funcName,
  54. 'hash': hashlib.sha256(password + cookie).hexdigest(),
  55. 'cookie': cookie,
  56. 'args': args,
  57. 'txid': txid
  58. }
  59. if password:
  60. req['aq'] = req['q']
  61. req['q'] = 'auth'
  62. reqBenc = bencode(req)
  63. req['hash'] = hashlib.sha256(reqBenc).hexdigest()
  64. reqBenc = bencode(req)
  65. sock.send(reqBenc)
  66. return _getMessage(session, txid)
  67. def _receiverThread(session):
  68. """Receiving messages from cjdns admin server"""
  69. timeOfLastSend = time.time()
  70. timeOfLastRecv = time.time()
  71. try:
  72. while True:
  73. if (timeOfLastSend + KEEPALIVE_INTERVAL_SECONDS < time.time()):
  74. if (timeOfLastRecv + 10 < time.time()):
  75. raise Exception("ping timeout")
  76. session.socket.send(
  77. 'd1:q18:Admin_asyncEnabled4:txid8:keepalive')
  78. timeOfLastSend = time.time()
  79. # Did we get data from the socket?
  80. got_data = False
  81. while True:
  82. # This can be interrupted and we need to loop it.
  83. try:
  84. data = session.socket.recv(BUFFER_SIZE)
  85. except (socket.timeout):
  86. # Stop retrying, but note we have no data
  87. break
  88. except socket.error as e:
  89. if e.errno != errno.EINTR:
  90. # Forward errors that aren't being interrupted
  91. raise
  92. # Otherwise it was interrupted so we try again.
  93. else:
  94. # Don't try again, we got data
  95. got_data = True
  96. break
  97. if not got_data:
  98. # Try asking again.
  99. continue
  100. try:
  101. benc = bdecode(data)
  102. except (KeyError, ValueError):
  103. print("error decoding [" + data + "]")
  104. continue
  105. if benc['txid'] == 'keepaliv':
  106. if benc['asyncEnabled'] == 0:
  107. raise Exception("lost session")
  108. timeOfLastRecv = time.time()
  109. else:
  110. # print "putting to queue " + str(benc)
  111. session.queue.put(benc)
  112. except KeyboardInterrupt:
  113. print("interrupted")
  114. import thread
  115. thread.interrupt_main()
  116. except Exception as e:
  117. # Forward along any errors, before killing the thread.
  118. session.queue.put(e)
  119. def _getMessage(session, txid):
  120. """Getting message associated with txid"""
  121. while True:
  122. if txid in session.messages:
  123. msg = session.messages[txid]
  124. del session.messages[txid]
  125. return msg
  126. else:
  127. # print "getting from queue"
  128. try:
  129. # apparently any timeout at all allows the thread to be
  130. # stopped but none make it unstoppable with ctrl+c
  131. next = session.queue.get(timeout=100)
  132. except Queue.Empty:
  133. continue
  134. if isinstance(next, Exception):
  135. # If the receiveing thread had an error, throw one here too.
  136. raise next
  137. if 'txid' in next:
  138. session.messages[next['txid']] = next
  139. # print "adding message [" + str(next) + "]"
  140. else:
  141. print "message with no txid: " + str(next)
  142. def _functionFabric(func_name, argList, oargs, oargNames, password):
  143. """Function fabric for Session class"""
  144. def functionHandler(self, *args, **kwargs):
  145. call_args = {}
  146. pos = 0
  147. for value in args:
  148. if (pos < len(argList)):
  149. call_args[argList[pos]] = value
  150. pos += 1
  151. elif (pos < len(argList) + len(oargNames)):
  152. call_args[oargNames[pos - len(argList)]] = value
  153. else:
  154. print("warning: extraneous argument passed to function",func_name,value)
  155. for (key, value) in kwargs.items():
  156. if key not in oargs:
  157. if key in argList:
  158. # this is a positional argument, given a keyword name
  159. # that happens in python.
  160. # TODO: we can't handle this along with unnamed positional args.
  161. pos = argList.index(key)
  162. call_args[argList[pos]] = value
  163. continue
  164. else:
  165. print("warning: not an argument to this function",func_name,key)
  166. print(oargs)
  167. else:
  168. # TODO: check oargs[key] type matches value
  169. # warn, if doesn't
  170. call_args[key] = value
  171. return _callFunc(self, func_name, password, call_args)
  172. functionHandler.__name__ = func_name
  173. return functionHandler
  174. def connect(ipAddr, port, password):
  175. """Connect to cjdns admin with this attributes"""
  176. sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
  177. sock.connect((ipAddr, port))
  178. sock.settimeout(2)
  179. # Make sure it pongs.
  180. sock.send('d1:q4:pinge')
  181. data = sock.recv(BUFFER_SIZE)
  182. if (not data.endswith('1:q4:ponge')):
  183. raise Exception(
  184. "Looks like " + ipAddr + ":" + str(port) +
  185. " is to a non-cjdns socket.")
  186. # Get the functions and make the object
  187. page = 0
  188. availableFunctions = {}
  189. while True:
  190. sock.send(
  191. 'd1:q24:Admin_availableFunctions4:argsd4:pagei' +
  192. str(page) + 'eee')
  193. data = sock.recv(BUFFER_SIZE)
  194. benc = bdecode(data)
  195. for func in benc['availableFunctions']:
  196. availableFunctions[func] = benc['availableFunctions'][func]
  197. if (not 'more' in benc):
  198. break
  199. page = page+1
  200. funcArgs = {}
  201. funcOargs = {}
  202. for (i, func) in availableFunctions.items():
  203. items = func.items()
  204. # required args
  205. argList = []
  206. # optional args
  207. oargs = {}
  208. # order of optional args for python-style calling
  209. oargNames = []
  210. for (arg,atts) in items:
  211. if atts['required']:
  212. argList.append(arg)
  213. else:
  214. oargs[arg] = atts['type']
  215. oargNames.append(arg)
  216. setattr(Session, i, _functionFabric(
  217. i, argList, oargs, oargNames, password))
  218. funcArgs[i] = argList
  219. funcOargs[i] = oargs
  220. session = Session(sock)
  221. kat = threading.Thread(target=_receiverThread, args=[session])
  222. kat.setDaemon(True)
  223. kat.start()
  224. # Check our password.
  225. ret = _callFunc(session, "ping", password, {})
  226. if ('error' in ret):
  227. raise Exception(
  228. "Connect failed, incorrect admin password?\n" + str(ret))
  229. session._functions = ""
  230. funcOargs_c = {}
  231. for func in funcOargs:
  232. funcOargs_c[func] = list(
  233. [key + "=" + str(value)
  234. for (key, value) in funcOargs[func].items()])
  235. for func in availableFunctions:
  236. session._functions += (
  237. func + "(" + ', '.join(funcArgs[func] + funcOargs_c[func]) + ")\n")
  238. # print session.functions
  239. return session
  240. def connectWithAdminInfo(path = None):
  241. """Connect to cjdns admin with data from user file"""
  242. if path is None:
  243. path = os.path.expanduser('~/.cjdnsadmin')
  244. try:
  245. with open(path, 'r') as adminInfo:
  246. data = json.load(adminInfo)
  247. except IOError:
  248. sys.stderr.write("""Please create a file named .cjdnsadmin in your
  249. home directory with
  250. ip, port, and password of your cjdns engine in json.
  251. for example:
  252. {
  253. "addr": "127.0.0.1",
  254. "port": 11234,
  255. "password": "You tell me! (Search in ~/cjdroute.conf)"
  256. }
  257. """)
  258. raise
  259. return connect(data['addr'], data['port'], data['password'])