test-updater.py 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. #!/usr/bin/env python3
  2. # requirements: pycryptodome
  3. from Crypto.PublicKey import ECC
  4. from Crypto.Signature import eddsa
  5. from hashlib import sha256
  6. from pathlib import Path
  7. import base64
  8. import configparser
  9. import gzip
  10. import http.server
  11. import json
  12. import os
  13. import shutil
  14. import socketserver
  15. import subprocess
  16. import sys
  17. import tempfile
  18. import threading
  19. import time
  20. UPDATE_KEY_TEST = ECC.construct(
  21. curve="Ed25519",
  22. seed=bytes.fromhex(
  23. "543a581db60008bbb978a464e136d686dbc9d594119e928b5276bece3d583d81"
  24. ),
  25. )
  26. HTTP_SERVER_ADDR = ("localhost", 8042)
  27. DOLPHIN_UPDATE_SERVER_URL = f"http://{HTTP_SERVER_ADDR[0]}:{HTTP_SERVER_ADDR[1]}"
  28. class Manifest:
  29. def __init__(self, path: Path):
  30. self.path = path
  31. self.entries = {}
  32. for p in self.path.glob("**/*.*"):
  33. if not p.is_file():
  34. continue
  35. digest = sha256(p.read_bytes()).digest()[:0x10].hex()
  36. self.entries[digest] = p.relative_to(self.path).as_posix()
  37. def get_signed(self):
  38. manifest = "".join(
  39. f"{name}\t{digest}\n" for digest, name in self.entries.items()
  40. )
  41. manifest = manifest.encode("utf-8")
  42. sig = eddsa.new(UPDATE_KEY_TEST, "rfc8032").sign(manifest)
  43. manifest += b"\n" + base64.b64encode(sig) + b"\n"
  44. return gzip.compress(manifest)
  45. def get_path(self, digest):
  46. return self.path.joinpath(self.entries.get(digest))
  47. class HTTPRequestHandler(http.server.BaseHTTPRequestHandler):
  48. def do_GET(self):
  49. if self.path.startswith("/update/check/v1/updater-test"):
  50. self.send_response(200)
  51. self.end_headers()
  52. self.wfile.write(
  53. bytes(
  54. json.dumps(
  55. {
  56. "status": "outdated",
  57. "content-store": DOLPHIN_UPDATE_SERVER_URL + "/content/",
  58. "changelog": [],
  59. "old": {"manifest": DOLPHIN_UPDATE_SERVER_URL + "/old"},
  60. "new": {
  61. "manifest": DOLPHIN_UPDATE_SERVER_URL + "/new",
  62. "name": "updater-test",
  63. "hash": bytes(range(32)).hex(),
  64. },
  65. }
  66. ),
  67. "utf-8",
  68. )
  69. )
  70. elif self.path == "/old":
  71. self.send_response(200)
  72. self.end_headers()
  73. self.wfile.write(self.current.get_signed())
  74. elif self.path == "/new":
  75. self.send_response(200)
  76. self.end_headers()
  77. self.wfile.write(self.next.get_signed())
  78. elif self.path.startswith("/content/"):
  79. self.send_response(200)
  80. self.end_headers()
  81. digest = "".join(self.path[len("/content/") :].split("/"))
  82. path = self.next.get_path(digest)
  83. self.wfile.write(gzip.compress(path.read_bytes()))
  84. elif self.path.startswith("/update-test-done/"):
  85. self.send_response(200)
  86. self.end_headers()
  87. HTTPRequestHandler.dolphin_pid = int(self.path[len("/update-test-done/") :])
  88. self.done.set()
  89. def http_server():
  90. with socketserver.TCPServer(HTTP_SERVER_ADDR, HTTPRequestHandler) as httpd:
  91. httpd.serve_forever()
  92. def create_entries_in_ini(ini_path: Path, entries: dict):
  93. config = configparser.ConfigParser()
  94. if ini_path.exists():
  95. config.read(ini_path)
  96. else:
  97. ini_path.parent.mkdir(parents=True, exist_ok=True)
  98. for section, options in entries.items():
  99. if not config.has_section(section):
  100. config.add_section(section)
  101. for option, value in options.items():
  102. config.set(section, option, value)
  103. with ini_path.open("w") as f:
  104. config.write(f)
  105. if __name__ == "__main__":
  106. dolphin_bin_path = Path(sys.argv[1])
  107. threading.Thread(target=http_server, daemon=True).start()
  108. with tempfile.TemporaryDirectory(suffix=" ¿ 🐬") as tmp_dir:
  109. tmp_dir = Path(tmp_dir)
  110. tmp_dolphin = tmp_dir.joinpath("dolphin")
  111. print(f"install to {tmp_dolphin}")
  112. shutil.copytree(dolphin_bin_path.parent, tmp_dolphin)
  113. tmp_dolphin.joinpath("portable.txt").touch()
  114. create_entries_in_ini(
  115. tmp_dolphin.joinpath("User/Config/Dolphin.ini"),
  116. {
  117. "Analytics": {"Enabled": "False", "PermissionAsked": "True"},
  118. "AutoUpdate": {"UpdateTrack": "updater-test"},
  119. },
  120. )
  121. tmp_dolphin_next = tmp_dir.joinpath("dolphin_next")
  122. print(f"install next to {tmp_dolphin_next}")
  123. # XXX copies from just-created dir so Dolphin.ini is kept
  124. shutil.copytree(tmp_dolphin, tmp_dolphin_next)
  125. tmp_dolphin_next.joinpath("updater-test-file").write_text("test")
  126. tmp_dolphin_next.joinpath("updater-test-filἑ").write_text("test")
  127. with tmp_dolphin_next.joinpath("build_info.txt").open("a") as f:
  128. print("test", file=f)
  129. for ext in ("exe", "dll"):
  130. for path in tmp_dolphin_next.glob("**/*." + ext):
  131. data = bytearray(path.read_bytes())
  132. richpos = data[:0x200].find(b"Rich")
  133. if richpos < 0:
  134. continue
  135. data[richpos : richpos + 4] = b"DOLP"
  136. path.write_bytes(data)
  137. HTTPRequestHandler.current = Manifest(tmp_dolphin)
  138. HTTPRequestHandler.next = Manifest(tmp_dolphin_next)
  139. HTTPRequestHandler.done = threading.Event()
  140. tmp_env = os.environ
  141. tmp_env.update({"DOLPHIN_UPDATE_SERVER_URL": DOLPHIN_UPDATE_SERVER_URL})
  142. tmp_dolphin_bin = tmp_dolphin.joinpath(dolphin_bin_path.name)
  143. result = subprocess.run(tmp_dolphin_bin, env=tmp_env)
  144. assert result.returncode == 0
  145. assert HTTPRequestHandler.done.wait(60 * 2)
  146. # works fine but raises exceptions...
  147. try:
  148. os.kill(HTTPRequestHandler.dolphin_pid, 0)
  149. except:
  150. pass
  151. try:
  152. os.waitpid(HTTPRequestHandler.dolphin_pid, 0)
  153. except:
  154. pass
  155. failed = False
  156. for path in tmp_dolphin_next.glob("**/*.*"):
  157. if not path.is_file():
  158. continue
  159. path_rel = path.relative_to(tmp_dolphin_next)
  160. if path_rel.parts[0] == "User":
  161. continue
  162. new_path = tmp_dolphin.joinpath(path_rel)
  163. if not new_path.exists():
  164. print(f"missing: {new_path}")
  165. failed = True
  166. continue
  167. if (
  168. sha256(new_path.read_bytes()).digest()
  169. != sha256(path.read_bytes()).digest()
  170. ):
  171. print(f"bad digest: {new_path} {path}")
  172. failed = True
  173. continue
  174. assert not failed
  175. print(tmp_dolphin.joinpath("User/Logs/Updater.log").read_text())
  176. # while True: time.sleep(1)