cjdnsadmin.py 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  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 <http://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, oargList, password):
  143. """Function fabric for Session class"""
  144. def functionHandler(self, *args, **kwargs):
  145. call_args = {}
  146. for (key, value) in oargList.items():
  147. call_args[key] = value
  148. for i, arg in enumerate(argList):
  149. if (i < len(args)):
  150. call_args[arg] = args[i]
  151. for (key, value) in kwargs.items():
  152. call_args[key] = value
  153. return _callFunc(self, func_name, password, call_args)
  154. functionHandler.__name__ = func_name
  155. return functionHandler
  156. def connect(ipAddr, port, password):
  157. """Connect to cjdns admin with this attributes"""
  158. sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
  159. sock.connect((ipAddr, port))
  160. sock.settimeout(2)
  161. # Make sure it pongs.
  162. sock.send('d1:q4:pinge')
  163. data = sock.recv(BUFFER_SIZE)
  164. if (not data.endswith('1:q4:ponge')):
  165. raise Exception(
  166. "Looks like " + ipAddr + ":" + str(port) +
  167. " is to a non-cjdns socket.")
  168. # Get the functions and make the object
  169. page = 0
  170. availableFunctions = {}
  171. while True:
  172. sock.send(
  173. 'd1:q24:Admin_availableFunctions4:argsd4:pagei' +
  174. str(page) + 'eee')
  175. data = sock.recv(BUFFER_SIZE)
  176. benc = bdecode(data)
  177. for func in benc['availableFunctions']:
  178. availableFunctions[func] = benc['availableFunctions'][func]
  179. if (not 'more' in benc):
  180. break
  181. page = page+1
  182. funcArgs = {}
  183. funcOargs = {}
  184. for (i, func) in availableFunctions.items():
  185. items = func.items()
  186. # grab all the required args first
  187. # append all the optional args
  188. rargList = [arg for arg,atts in items if atts['required']]
  189. argList = rargList + [arg for arg,atts in items if not atts['required']]
  190. # for each optional arg setup a default value with
  191. # a type which will be ignored by the core.
  192. oargList = {}
  193. for (arg,atts) in items:
  194. if not atts['required']:
  195. oargList[arg] = (
  196. "''" if (func[arg]['type'] == 'Int')
  197. else "0")
  198. setattr(Session, i, _functionFabric(
  199. i, argList, oargList, password))
  200. funcArgs[i] = rargList
  201. funcOargs[i] = oargList
  202. session = Session(sock)
  203. kat = threading.Thread(target=_receiverThread, args=[session])
  204. kat.setDaemon(True)
  205. kat.start()
  206. # Check our password.
  207. ret = _callFunc(session, "ping", password, {})
  208. if ('error' in ret):
  209. raise Exception(
  210. "Connect failed, incorrect admin password?\n" + str(ret))
  211. session._functions = ""
  212. funcOargs_c = {}
  213. for func in funcOargs:
  214. funcOargs_c[func] = list(
  215. [key + "=" + str(value)
  216. for (key, value) in funcOargs[func].items()])
  217. for func in availableFunctions:
  218. session._functions += (
  219. func + "(" + ', '.join(funcArgs[func] + funcOargs_c[func]) + ")\n")
  220. # print session.functions
  221. return session
  222. def connectWithAdminInfo(path = None):
  223. """Connect to cjdns admin with data from user file"""
  224. if path is None:
  225. path = os.path.expanduser('~/.cjdnsadmin')
  226. try:
  227. with open(path, 'r') as adminInfo:
  228. data = json.load(adminInfo)
  229. except IOError:
  230. sys.stderr.write("""Please create a file named .cjdnsadmin in your
  231. home directory with
  232. ip, port, and password of your cjdns engine in json.
  233. for example:
  234. {
  235. "addr": "127.0.0.1",
  236. "port": 11234,
  237. "password": "You tell me! (Search in ~/cjdroute.conf)"
  238. }
  239. """)
  240. raise
  241. return connect(data['addr'], data['port'], data['password'])