main.py 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  1. import os
  2. import os.path
  3. import sys
  4. import time
  5. import queue
  6. import argparse
  7. import tempfile
  8. import threading
  9. import cv2
  10. import requests
  11. from loguru import logger
  12. from Crypto.Hash import BLAKE2b
  13. try:
  14. import rdrand
  15. except ImportError:
  16. logger.warning("RdRand is not available.")
  17. class TemporaryFile:
  18. def __init__(self, name, io, delete):
  19. self.name = name
  20. self.__io = io
  21. self.__delete = delete
  22. def __getattr__(self, k):
  23. return getattr(self.__io, k)
  24. def __del__(self):
  25. if self.__delete:
  26. try:
  27. os.unlink(self.name)
  28. except FileNotFoundError:
  29. pass
  30. def NamedTemporaryFile(
  31. mode="w+b", bufsize=-1, suffix="", prefix="tmp", dir=None, delete=True
  32. ):
  33. if not dir:
  34. dir = tempfile.gettempdir()
  35. name = os.path.join(dir, prefix + os.urandom(32).hex() + suffix)
  36. if mode is None:
  37. return TemporaryFile(name, None, delete)
  38. fh = open(name, "w+b", bufsize)
  39. if mode != "w+b":
  40. fh.close()
  41. fh = open(name, mode)
  42. return TemporaryFile(name, fh, delete)
  43. def chunks(lst, n):
  44. for i in range(0, len(lst), n):
  45. yield lst[i : i + n]
  46. def run_check(cmd):
  47. logger.info(f"Executing '{cmd}'.")
  48. if os.system(cmd) != 0:
  49. raise ValueError("Exit code != 0.")
  50. def extract_image(path):
  51. logger.info(f"Extract image '{path}'.")
  52. im = cv2.imread(path)
  53. data = []
  54. rows, cols, _ = im.shape
  55. for i in range(rows):
  56. for j in range(cols):
  57. r, g, b = im[i, j]
  58. data.append(((r << 16) + (g << 8) + b) & 255)
  59. return bytes(data)
  60. def extract_wav(path):
  61. logger.info(f"Extract audio: '{path}'.")
  62. data = []
  63. with open(path, "rb") as f:
  64. for sample in chunks(f.read()[44:], 2):
  65. data.append(sample[0])
  66. return bytes(data)
  67. def extract_lsbs(data):
  68. logger.info("Extract LSBs.")
  69. buffer = []
  70. if len(data) % 2 != 0:
  71. data = data[:-1]
  72. for chunk in chunks(data, 2):
  73. tmp_byte = 0
  74. for byte in chunk:
  75. for n in range(4):
  76. tmp_byte = (tmp_byte << 1) | ((byte >> n) & 1)
  77. buffer.append(tmp_byte & 255)
  78. return bytes(buffer)
  79. def whiten(data):
  80. logger.info("Whitening.")
  81. buffer = b""
  82. for chunk in chunks(data, 128):
  83. buffer += BLAKE2b.new(data=chunk, digest_bits=256).digest()
  84. return buffer
  85. def read_video(source, duration=60):
  86. tmpf = NamedTemporaryFile(suffix=".mkv", mode=None)
  87. run_check(
  88. f"ffmpeg -hide_banner -loglevel error -y -i {source} -t {duration} -acodec copy -vcodec copy {tmpf.name}"
  89. )
  90. with tempfile.TemporaryDirectory() as tmpd:
  91. run_check(
  92. f"ffmpeg -hide_banner -loglevel error -y -i {tmpf.name} -vf mpdecimate -r 1/1 {tmpd}/%d.bmp"
  93. )
  94. data = b""
  95. for filename in os.listdir(tmpd):
  96. data += extract_image(os.path.join(tmpd, filename))
  97. return data
  98. def read_audio(source, duration=60):
  99. tmpf = NamedTemporaryFile(suffix=".wav", mode=None)
  100. run_check(
  101. f"ffmpeg -f alsa -i {source} -t {duration} -ar 48000 -f s16le -acodec pcm_s16le {tmpf.name}"
  102. )
  103. return extract_wav(tmpf.name)
  104. def read_audio_video(source, duration=60):
  105. tmpf = NamedTemporaryFile(suffix=".mkv", mode=None)
  106. run_check(
  107. f"ffmpeg -hide_banner -loglevel error -y -i {source} -t {duration} -acodec copy -vcodec copy {tmpf.name}"
  108. )
  109. with tempfile.TemporaryDirectory() as tmpd:
  110. run_check(
  111. f"ffmpeg -hide_banner -loglevel error -y -i {tmpf.name} -vf mpdecimate -r 1/1 {tmpd}/%d.bmp"
  112. )
  113. data_a = b""
  114. for filename in sorted(
  115. os.listdir(tmpd), key=lambda filename: int(filename.split(".")[0])
  116. ):
  117. data_a += extract_image(os.path.join(tmpd, filename))
  118. tmpf2 = NamedTemporaryFile(suffix=".wav", mode=None)
  119. run_check(
  120. f"ffmpeg -hide_banner -loglevel error -y -i {tmpf.name} -vn -ar 48000 -f s16le -acodec pcm_s16le {tmpf2.name}"
  121. )
  122. data_b = extract_wav(tmpf2.name)
  123. return bytes(a ^ b for a, b in zip(data_a, data_b))
  124. def read_rdseed(_, amount=16):
  125. data = rdrand.rdseed_get_bytes(amount)
  126. if len(data) != amount or data.count(0) == amount:
  127. raise ValueError("bad data")
  128. return data
  129. def sample(source, source_type, multiplier=1):
  130. match source_type:
  131. case "video":
  132. sampler = read_video
  133. multiplier *= 60
  134. case "audio":
  135. sampler = read_audio
  136. multiplier *= 60
  137. case "video+audio":
  138. sampler = read_audio_video
  139. multiplier *= 60
  140. case "rdseed":
  141. sampler = read_rdseed
  142. case _:
  143. raise ValueError(source_type)
  144. multiplier = int(multiplier)
  145. if multiplier < 1:
  146. raise ValueError(multiplier)
  147. logger.info("Sampling...")
  148. data = sampler(source, multiplier)
  149. logger.info(f"Sample ready: {len(data)}b.")
  150. if source_type != "rdseed":
  151. data = extract_lsbs(data)
  152. data = whiten(data)
  153. return data
  154. def push(pool_url, data, secret):
  155. logger.info(f"Pushing {len(data)}b.")
  156. resp = requests.post(
  157. f"{pool_url}/api/pool", data=data, headers={"X-Secret": secret}
  158. )
  159. (logger.success if resp.status_code == 200 else logger.error)(
  160. f"{resp.status_code}: {resp.text}"
  161. )
  162. def puller(queue, source, source_type, multiplier):
  163. while True:
  164. try:
  165. data = sample(source, source_type, multiplier)
  166. except KeyboardInterrupt:
  167. logger.info("Interrupted by user.")
  168. sys.exit(0)
  169. except Exception as e:
  170. logger.error(f"Pull exception: {e}")
  171. continue
  172. for piece in chunks(data, 1024 * 500):
  173. queue.put(piece)
  174. def pusher(queue, pool_url, secret, cooldown=0):
  175. while True:
  176. piece = queue.get()
  177. try:
  178. push(pool_url, piece, secret)
  179. except KeyboardInterrupt:
  180. logger.info("Interrupted by user.")
  181. sys.exit(0)
  182. except Exception as e:
  183. logger.error(f"Push exception: {e}")
  184. if cooldown:
  185. time.sleep(cooldown)
  186. if __name__ == "__main__":
  187. parser = argparse.ArgumentParser()
  188. parser.add_argument("--source", type=str, required=True)
  189. parser.add_argument("--source-type", type=str, default="video+audio")
  190. parser.add_argument("--multiplier", type=float, default=1)
  191. parser.add_argument("--secret-file", type=str, default="./.secret")
  192. parser.add_argument("--cooldown", type=int, default=0)
  193. parser.add_argument("--pool-url", type=str, default="https://trng.iike.ru")
  194. args = parser.parse_args()
  195. with open(args.secret_file, "r") as f:
  196. lines = f.read().strip().split("\n")
  197. ident = lines[0].strip()
  198. secret = lines[1].strip()
  199. secret = f"{ident} {secret}"
  200. q = queue.Queue()
  201. threading.Thread(
  202. target=puller, args=(q, args.source, args.source_type, args.multiplier)
  203. ).start()
  204. pusher_th = threading.Thread(
  205. target=pusher, args=(q, args.pool_url, secret, args.cooldown)
  206. )
  207. pusher_th.start()
  208. pusher_th.join()