cjdnsadmin.py 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  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 hashlib
  16. import json
  17. import threading
  18. import time
  19. import Queue
  20. import random
  21. import string
  22. from bencode import *
  23. BUFFER_SIZE = 69632
  24. KEEPALIVE_INTERVAL_SECONDS = 2
  25. class Session():
  26. """Current cjdns admin session"""
  27. def __init__(self, socket):
  28. self.socket = socket
  29. self.queue = Queue.Queue()
  30. self.messages = {}
  31. def disconnect(self):
  32. self.socket.close()
  33. def getMessage(self, txid):
  34. # print self, txid
  35. return _getMessage(self, txid)
  36. def functions(self):
  37. print(self._functions)
  38. def _randomString():
  39. """Random string for message signing"""
  40. return ''.join(
  41. random.choice(string.ascii_uppercase + string.digits)
  42. for x in range(10))
  43. def _callFunc(session, funcName, password, args):
  44. """Call custom cjdns admin function"""
  45. txid = _randomString()
  46. sock = session.socket
  47. sock.send('d1:q6:cookie4:txid10:' + txid + 'e')
  48. msg = _getMessage(session, txid)
  49. cookie = msg['cookie']
  50. txid = _randomString()
  51. req = {
  52. 'q': 'auth',
  53. 'aq': funcName,
  54. 'hash': hashlib.sha256(password + cookie).hexdigest(),
  55. 'cookie': cookie,
  56. 'args': args,
  57. 'txid': txid
  58. }
  59. reqBenc = bencode(req)
  60. req['hash'] = hashlib.sha256(reqBenc).hexdigest()
  61. reqBenc = bencode(req)
  62. sock.send(reqBenc)
  63. return _getMessage(session, txid)
  64. def _receiverThread(session):
  65. """Receiving messages from cjdns admin server"""
  66. timeOfLastSend = time.time()
  67. timeOfLastRecv = time.time()
  68. try:
  69. while True:
  70. if (timeOfLastSend + KEEPALIVE_INTERVAL_SECONDS < time.time()):
  71. if (timeOfLastRecv + 10 < time.time()):
  72. raise Exception("ping timeout")
  73. session.socket.send(
  74. 'd1:q18:Admin_asyncEnabled4:txid8:keepalive')
  75. timeOfLastSend = time.time()
  76. try:
  77. data = session.socket.recv(BUFFER_SIZE)
  78. except (socket.timeout):
  79. continue
  80. try:
  81. benc = bdecode(data)
  82. except (KeyError, ValueError):
  83. print("error decoding [" + data + "]")
  84. continue
  85. if benc['txid'] == 'keepaliv':
  86. if benc['asyncEnabled'] == 0:
  87. raise Exception("lost session")
  88. timeOfLastRecv = time.time()
  89. else:
  90. # print "putting to queue " + str(benc)
  91. session.queue.put(benc)
  92. except KeyboardInterrupt:
  93. print("interrupted")
  94. import thread
  95. thread.interrupt_main()
  96. def _getMessage(session, txid):
  97. """Getting message associated with txid"""
  98. while True:
  99. if txid in session.messages:
  100. msg = session.messages[txid]
  101. del session.messages[txid]
  102. return msg
  103. else:
  104. # print "getting from queue"
  105. try:
  106. # apparently any timeout at all allows the thread to be
  107. # stopped but none make it unstoppable with ctrl+c
  108. next = session.queue.get(timeout=100)
  109. except Queue.Empty:
  110. continue
  111. if 'txid' in next:
  112. session.messages[next['txid']] = next
  113. # print "adding message [" + str(next) + "]"
  114. else:
  115. print "message with no txid: " + str(next)
  116. def _functionFabric(func_name, argList, oargList, password):
  117. """Function fabric for Session class"""
  118. def functionHandler(self, *args, **kwargs):
  119. call_args = {}
  120. for (key, value) in oargList.items():
  121. call_args[key] = value
  122. for i, arg in enumerate(argList):
  123. if (i < len(args)):
  124. call_args[arg] = args[i]
  125. for (key, value) in kwargs.items():
  126. call_args[key] = value
  127. return _callFunc(self, func_name, password, call_args)
  128. functionHandler.__name__ = func_name
  129. return functionHandler
  130. def connect(ipAddr, port, password):
  131. """Connect to cjdns admin with this attributes"""
  132. sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
  133. sock.connect((ipAddr, port))
  134. sock.settimeout(2)
  135. # Make sure it pongs.
  136. sock.send('d1:q4:pinge')
  137. data = sock.recv(BUFFER_SIZE)
  138. if (not data.endswith('1:q4:ponge')):
  139. raise Exception(
  140. "Looks like " + ipAddr + ":" + str(port) +
  141. " is to a non-cjdns socket.")
  142. # Get the functions and make the object
  143. page = 0
  144. availableFunctions = {}
  145. while True:
  146. sock.send(
  147. 'd1:q24:Admin_availableFunctions4:argsd4:pagei' +
  148. str(page) + 'eee')
  149. data = sock.recv(BUFFER_SIZE)
  150. benc = bdecode(data)
  151. for func in benc['availableFunctions']:
  152. availableFunctions[func] = benc['availableFunctions'][func]
  153. if (not 'more' in benc):
  154. break
  155. page = page+1
  156. funcArgs = {}
  157. funcOargs = {}
  158. for (i, func) in availableFunctions.items():
  159. items = func.items()
  160. # grab all the required args first
  161. # append all the optional args
  162. rargList = [arg for arg,atts in items if atts['required']]
  163. argList = rargList + [arg for arg,atts in items if not atts['required']]
  164. # for each optional arg setup a default value with
  165. # a type which will be ignored by the core.
  166. oargList = {}
  167. for (arg,atts) in items:
  168. if not atts['required']:
  169. oargList[arg] = (
  170. "''" if (func[arg]['type'] == 'Int')
  171. else "0")
  172. setattr(Session, i, _functionFabric(
  173. i, argList, oargList, password))
  174. funcArgs[i] = rargList
  175. funcOargs[i] = oargList
  176. session = Session(sock)
  177. kat = threading.Thread(target=_receiverThread, args=[session])
  178. kat.setDaemon(True)
  179. kat.start()
  180. # Check our password.
  181. ret = _callFunc(session, "ping", password, {})
  182. if ('error' in ret):
  183. raise Exception(
  184. "Connect failed, incorrect admin password?\n" + str(ret))
  185. session._functions = ""
  186. funcOargs_c = {}
  187. for func in funcOargs:
  188. funcOargs_c[func] = list(
  189. [key + "=" + str(value)
  190. for (key, value) in funcOargs[func].items()])
  191. for func in availableFunctions:
  192. session._functions += (
  193. func + "(" + ', '.join(funcArgs[func] + funcOargs_c[func]) + ")\n")
  194. # print session.functions
  195. return session
  196. def connectWithAdminInfo(path = None):
  197. """Connect to cjdns admin with data from user file"""
  198. if path is None:
  199. path = os.path.expanduser('~/.cjdnsadmin')
  200. try:
  201. with open(path, 'r') as adminInfo:
  202. data = json.load(adminInfo)
  203. except IOError:
  204. print('Please create a file named .cjdnsadmin in your ')
  205. print('home directory with')
  206. print('ip, port, and password of your cjdns engine in json.')
  207. print('for example:')
  208. print('{')
  209. print(' "addr": "127.0.0.1",')
  210. print(' "port": 11234,')
  211. print(' "password": "You tell me! (Search in ~/cjdroute.conf)"')
  212. print('}')
  213. raise
  214. return connect(data['addr'], data['port'], data['password'])