cjdnsadmin.py 7.7 KB

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