summaryrefslogtreecommitdiff
path: root/Tools/test-updater.py
blob: 638bd4fac7e5e972e50876f1a804b72934e0934a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
#!/usr/bin/env python3
# requirements: pycryptodome
from Crypto.PublicKey import ECC
from Crypto.Signature import eddsa
from hashlib import sha256
from pathlib import Path
import base64
import configparser
import gzip
import http.server
import json
import os
import shutil
import socketserver
import subprocess
import sys
import tempfile
import threading
import time

UPDATE_KEY_TEST = ECC.construct(
    curve="Ed25519",
    seed=bytes.fromhex(
        "543a581db60008bbb978a464e136d686dbc9d594119e928b5276bece3d583d81"
    ),
)

HTTP_SERVER_ADDR = ("localhost", 8042)
DOLPHIN_UPDATE_SERVER_URL = f"http://{HTTP_SERVER_ADDR[0]}:{HTTP_SERVER_ADDR[1]}"


class Manifest:
    def __init__(self, path: Path):
        self.path = path
        self.entries = {}
        for p in self.path.glob("**/*.*"):
            if not p.is_file():
                continue
            digest = sha256(p.read_bytes()).digest()[:0x10].hex()
            self.entries[digest] = p.relative_to(self.path).as_posix()

    def get_signed(self):
        manifest = "".join(
            f"{name}\t{digest}\n" for digest, name in self.entries.items()
        )
        manifest = manifest.encode("utf-8")
        sig = eddsa.new(UPDATE_KEY_TEST, "rfc8032").sign(manifest)
        manifest += b"\n" + base64.b64encode(sig) + b"\n"
        return gzip.compress(manifest)

    def get_path(self, digest):
        return self.path.joinpath(self.entries.get(digest))


class HTTPRequestHandler(http.server.BaseHTTPRequestHandler):
    def do_GET(self):
        if self.path.startswith("/update/check/v1/updater-test"):
            self.send_response(200)
            self.end_headers()
            self.wfile.write(
                bytes(
                    json.dumps(
                        {
                            "status": "outdated",
                            "content-store": DOLPHIN_UPDATE_SERVER_URL + "/content/",
                            "changelog": [],
                            "old": {"manifest": DOLPHIN_UPDATE_SERVER_URL + "/old"},
                            "new": {
                                "manifest": DOLPHIN_UPDATE_SERVER_URL + "/new",
                                "name": "updater-test",
                                "hash": bytes(range(32)).hex(),
                            },
                        }
                    ),
                    "utf-8",
                )
            )
        elif self.path == "/old":
            self.send_response(200)
            self.end_headers()
            self.wfile.write(self.current.get_signed())
        elif self.path == "/new":
            self.send_response(200)
            self.end_headers()
            self.wfile.write(self.next.get_signed())
        elif self.path.startswith("/content/"):
            self.send_response(200)
            self.end_headers()
            digest = "".join(self.path[len("/content/") :].split("/"))
            path = self.next.get_path(digest)
            self.wfile.write(gzip.compress(path.read_bytes()))
        elif self.path.startswith("/update-test-done/"):
            self.send_response(200)
            self.end_headers()
            HTTPRequestHandler.dolphin_pid = int(self.path[len("/update-test-done/") :])
            self.done.set()


def http_server():
    with socketserver.TCPServer(HTTP_SERVER_ADDR, HTTPRequestHandler) as httpd:
        httpd.serve_forever()


def create_entries_in_ini(ini_path: Path, entries: dict):
    config = configparser.ConfigParser()
    if ini_path.exists():
        config.read(ini_path)
    else:
        ini_path.parent.mkdir(parents=True, exist_ok=True)

    for section, options in entries.items():
        if not config.has_section(section):
            config.add_section(section)
        for option, value in options.items():
            config.set(section, option, value)

    with ini_path.open("w") as f:
        config.write(f)


if __name__ == "__main__":
    dolphin_bin_path = Path(sys.argv[1])

    threading.Thread(target=http_server, daemon=True).start()

    with tempfile.TemporaryDirectory(suffix=" ¿ 🐬") as tmp_dir:
        tmp_dir = Path(tmp_dir)

        tmp_dolphin = tmp_dir.joinpath("dolphin")
        print(f"install to {tmp_dolphin}")
        shutil.copytree(dolphin_bin_path.parent, tmp_dolphin)
        tmp_dolphin.joinpath("portable.txt").touch()
        create_entries_in_ini(
            tmp_dolphin.joinpath("User/Config/Dolphin.ini"),
            {
                "Analytics": {"Enabled": "False", "PermissionAsked": "True"},
                "AutoUpdate": {"UpdateTrack": "updater-test"},
            },
        )

        tmp_dolphin_next = tmp_dir.joinpath("dolphin_next")
        print(f"install next to {tmp_dolphin_next}")
        # XXX copies from just-created dir so Dolphin.ini is kept
        shutil.copytree(tmp_dolphin, tmp_dolphin_next)
        tmp_dolphin_next.joinpath("updater-test-file").write_text("test")
        tmp_dolphin_next.joinpath("updater-test-filἑ").write_text("test")
        with tmp_dolphin_next.joinpath("build_info.txt").open("a") as f:
            print("test", file=f)
        for ext in ("exe", "dll"):
            for path in tmp_dolphin_next.glob("**/*." + ext):
                data = bytearray(path.read_bytes())
                richpos = data[:0x200].find(b"Rich")
                if richpos < 0:
                    continue
                data[richpos : richpos + 4] = b"DOLP"
                path.write_bytes(data)

        HTTPRequestHandler.current = Manifest(tmp_dolphin)
        HTTPRequestHandler.next = Manifest(tmp_dolphin_next)
        HTTPRequestHandler.done = threading.Event()

        tmp_env = os.environ
        tmp_env.update({"DOLPHIN_UPDATE_SERVER_URL": DOLPHIN_UPDATE_SERVER_URL})
        tmp_dolphin_bin = tmp_dolphin.joinpath(dolphin_bin_path.name)
        result = subprocess.run(tmp_dolphin_bin, env=tmp_env)
        assert result.returncode == 0

        assert HTTPRequestHandler.done.wait(60 * 2)
        # works fine but raises exceptions...
        try:
            os.kill(HTTPRequestHandler.dolphin_pid, 0)
        except:
            pass
        try:
            os.waitpid(HTTPRequestHandler.dolphin_pid, 0)
        except:
            pass

        failed = False
        for path in tmp_dolphin_next.glob("**/*.*"):
            if not path.is_file():
                continue
            path_rel = path.relative_to(tmp_dolphin_next)
            if path_rel.parts[0] == "User":
                continue
            new_path = tmp_dolphin.joinpath(path_rel)
            if not new_path.exists():
                print(f"missing: {new_path}")
                failed = True
                continue
            if (
                sha256(new_path.read_bytes()).digest()
                != sha256(path.read_bytes()).digest()
            ):
                print(f"bad digest: {new_path} {path}")
                failed = True
                continue
        assert not failed

        print(tmp_dolphin.joinpath("User/Logs/Updater.log").read_text())
        # while True: time.sleep(1)