byafnctl.py 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372
  1. import re
  2. import os
  3. import os.path
  4. import sys
  5. import time
  6. import struct
  7. import socket
  8. import pathlib
  9. import logging
  10. import argparse
  11. import tqdm
  12. import cbor2
  13. from Crypto.Hash import SHA256
  14. from Crypto.Cipher import Salsa20
  15. from Crypto.Random import get_random_bytes
  16. logging.basicConfig(
  17. format="%(asctime)s %(levelname)s: %(message)s",
  18. level=logging.INFO
  19. )
  20. class Metafile:
  21. HEADER = b'\x80BYAFN-METAFILE\x00\x00'
  22. HEADER_LEN = len(HEADER)
  23. def __init__(self, filename, size, checksum, pieces, key=None):
  24. self.filename = filename
  25. self.size = size
  26. self.checksum = checksum
  27. self.pieces = pieces
  28. self.key = key
  29. def save(self, f):
  30. f.write(Metafile.HEADER)
  31. filename = self.filename.encode('UTF-8')
  32. filename_len = len(filename)
  33. f.write(struct.pack('!I', filename_len))
  34. f.write(filename)
  35. f.write(struct.pack('!L', self.size))
  36. f.write(self.checksum)
  37. if self.key:
  38. f.write(b'Y')
  39. f.write(self.key)
  40. else:
  41. f.write(b'N')
  42. pieces_count = len(self.pieces)
  43. f.write(struct.pack('!L', pieces_count))
  44. for hash in self.pieces:
  45. f.write(hash)
  46. @staticmethod
  47. def load(f):
  48. header = f.read(Metafile.HEADER_LEN)
  49. if header != Metafile.HEADER:
  50. raise ValueError
  51. filename_len = f.read(4)
  52. filename_len = struct.unpack('!I', filename_len)[0]
  53. filename = f.read(filename_len)
  54. if len(filename) != filename_len:
  55. raise ValueError
  56. filename = filename.decode('UTF-8')
  57. filename = pathlib.Path(filename).name
  58. size = f.read(4)
  59. size = struct.unpack('!I', size)[0]
  60. checksum = f.read(32)
  61. if len(checksum) != 32:
  62. raise ValueError
  63. is_encrypted = f.read(1)
  64. if is_encrypted == b'Y':
  65. key = f.read(40)
  66. if len(key) != 40:
  67. raise ValueError
  68. elif is_encrypted == b'N':
  69. key = None
  70. else:
  71. raise ValueError
  72. pieces_count = f.read(4)
  73. pieces_count = struct.unpack('!L', pieces_count)[0]
  74. if pieces_count < 1:
  75. raise ValueError
  76. pieces = []
  77. while pieces_count > 0:
  78. piece_hash = f.read(32)
  79. if len(piece_hash) != 32:
  80. raise ValueError
  81. pieces.append(piece_hash)
  82. pieces_count -= 1
  83. return Metafile(filename, size, checksum, pieces, key)
  84. def sha256(data):
  85. hash = SHA256.new()
  86. hash.update(data)
  87. return hash.digest()
  88. def salsa20_create_encryptor():
  89. key = get_random_bytes(32)
  90. cipher = Salsa20.new(key=key)
  91. return (cipher.nonce + key, cipher)
  92. def salsa20_create_decryptor(key):
  93. if len(key) != 40:
  94. raise ValueError
  95. cipher = Salsa20.new(key=key[8:], nonce=key[:8])
  96. return cipher
  97. def parse_file_size(size):
  98. units = {
  99. 'b': 1,
  100. 'kb': 10e3,
  101. 'mb': 10e6
  102. }
  103. match = re.match(r'(\d+)([km]b)', size.strip().lower())
  104. if not match:
  105. raise ValueError
  106. size, unit = int(match.group(1)), units[match.group(2)]
  107. return int(size * unit)
  108. def send_command(**command):
  109. try:
  110. conn = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
  111. conn.connect(args.admin_socket_path)
  112. command = cbor2.dumps(command)
  113. conn.send(struct.pack('<I', len(command)))
  114. conn.send(command)
  115. length = struct.unpack('<I', conn.recv(4))[0]
  116. response = conn.recv(length)
  117. response = cbor2.loads(response)
  118. conn.close()
  119. except Exception as e:
  120. logging.error(f'Contacting the admin socket `{args.admin_socket_path}\': {e}')
  121. sys.exit(1)
  122. return response
  123. parser = argparse.ArgumentParser()
  124. parser.add_argument(
  125. '-G', '--genconf',
  126. help='Generate configuration file',
  127. action='store_true'
  128. )
  129. parser.add_argument(
  130. '--listen-address',
  131. help='Set ListenAddress (use together with --genconf)',
  132. type=str, default='0.0.0.0'
  133. )
  134. parser.add_argument(
  135. '-p', '--listen-port',
  136. help='Set ListenPort (use together with --genconf)',
  137. type=int, default=42424
  138. )
  139. parser.add_argument(
  140. '-s', '--storage-path',
  141. help='Set StoragePath (use together with --genconf)',
  142. type=str, default='./storage'
  143. )
  144. parser.add_argument(
  145. '-a', '--admin-socket-path',
  146. help='Set AdminSocketPath (use together with --genconf, --share or --query)',
  147. type=str, default='./adminsocket'
  148. )
  149. parser.add_argument(
  150. '-S', '--share',
  151. help='Share a file',
  152. type=argparse.FileType('rb')
  153. )
  154. parser.add_argument(
  155. '-e', '--encrypt',
  156. help='Encrypt file before sharing (use together with --share)',
  157. action='store_true'
  158. )
  159. parser.add_argument(
  160. '-Q', '--query',
  161. help='Query a file',
  162. type=argparse.FileType('rb')
  163. )
  164. parser.add_argument(
  165. '-o', '--output',
  166. help='Set output file',
  167. type=argparse.FileType('wb')
  168. )
  169. parser.add_argument(
  170. '-P', '--piece-size',
  171. help='Set piece size',
  172. type=parse_file_size, default='16kb'
  173. )
  174. parser.add_argument(
  175. '--shutdown',
  176. help='Request graceful shutdown of the peer',
  177. type=int
  178. )
  179. args = parser.parse_args()
  180. if args.genconf:
  181. import ecies
  182. key = ecies.utils.generate_eth_key()
  183. pk = key.public_key.to_hex()
  184. sk = key.to_hex()
  185. if not args.output:
  186. output = sys.stdout.buffer
  187. else:
  188. output = args.output
  189. output.write(bytes(f'''{{
  190. Peers: []
  191. ListenAddress: {args.listen_address}
  192. ListenPort: {args.listen_port}
  193. Key: {pk}
  194. Secret: {sk}
  195. StoragePath: {os.path.abspath(args.storage_path)}
  196. AdminSocketPath: {os.path.abspath(args.admin_socket_path)}
  197. }}''', 'UTF-8'))
  198. if args.share:
  199. with args.share as f:
  200. filename = pathlib.Path(f.name).name
  201. f.seek(0, os.SEEK_END)
  202. file_size = f.tell()
  203. f.seek(0)
  204. checksum = SHA256.new()
  205. if args.encrypt:
  206. key, cipher = salsa20_create_encryptor()
  207. pieces_count = max(1, file_size // args.piece_size)
  208. progress = tqdm.trange(pieces_count)
  209. pieces = []
  210. while True:
  211. piece = f.read(args.piece_size)
  212. if not piece:
  213. break
  214. checksum.update(piece)
  215. if args.encrypt:
  216. piece = cipher.encrypt(piece)
  217. hash = send_command(store={
  218. 'piece': piece
  219. })['hash']
  220. pieces.append(hash)
  221. progress.update(1)
  222. metafile = Metafile(
  223. filename,
  224. file_size,
  225. checksum.digest(),
  226. pieces,
  227. key=key if args.encrypt else None
  228. )
  229. if not args.output:
  230. output = open(f'{filename}.byafn', 'wb')
  231. else:
  232. output = args.output
  233. metafile.save(output)
  234. output.close()
  235. if args.query:
  236. with args.query as f:
  237. metafile = Metafile.load(f)
  238. if not args.output:
  239. output = open(metafile.filename, 'wb')
  240. else:
  241. output = args.output
  242. if metafile.key:
  243. cipher = salsa20_create_decryptor(metafile.key)
  244. checksum = SHA256.new()
  245. progress = tqdm.trange(len(metafile.pieces))
  246. for piece in metafile.pieces:
  247. interval = 10
  248. while True:
  249. data = send_command(query={
  250. 'hash': piece
  251. })
  252. if 'piece' not in data:
  253. logging.error(f'Piece `{piece.hex()}\' is not available')
  254. logging.info(f'Retrying in {interval}sec.')
  255. time.sleep(interval)
  256. if interval < 120:
  257. interval += 10
  258. continue
  259. break
  260. piece = data['piece']
  261. if metafile.key:
  262. piece = cipher.decrypt(piece)
  263. checksum.update(piece)
  264. if checksum.digest() != metafile.checksum:
  265. logging.error('Integrity check failed')
  266. sys.exit(1)
  267. output.write(piece)
  268. progress.update(1)
  269. output.close()
  270. if args.shutdown:
  271. send_command(shutdown={
  272. 'delay': args.shutdown
  273. })
  274. logging.info('Shutdown command sent')