400 lines13 KBC
1/*
2https_test.c: the app's HTTPS client (src/ps3/http_ps3.c, the same Mbed TLS
3build and root certificates) run on Linux against dl.ps3.world, before any UI
4is built on it. CI's "HTTPS test" job; see .github/workflows/ps3.yml.
5
6 ./https_test tests/sample
7
81. a whole file, streamed through the sink, against its known sha256
92. the same file resumed with Range from the middle: 206, and the two parts
10 together hash to the same sha256
113. a missing file: a 404 that comes back as a status, not a hang or a crash
124. a server with an expired certificate: refused
135. catalog.json: the live one if it's been uploaded, and the sample, parsed
146. download.c: a whole file to disk; one stopped part way and resumed with
15 Range; a wrong sha256 (deleted); not enough space; a 404; a cancel
16
17Each result is also printed as a GitHub annotation, readable without signing in.
18*/
19#include <stdio.h>
20#include <stdlib.h>
21#include <string.h>
22#include <stdarg.h>
23#include <pthread.h>
24#include <unistd.h>
25#include <dirent.h>
26#include <sys/stat.h>
27#include <sys/statvfs.h>
28#include "mbedtls/sha256.h"
29#include "plat.h"
30#include "app.h"
31#include "catalog.h"
32#include "download.h"
33
34int ps3_net_ok = 1;
35
36// ---- the plat_ file functions, on Linux ----
37unsigned char *plat_read_file(const char *path, size_t *len)
38{
39 FILE *f = fopen(path, "rb");
40 if (!f) return NULL;
41 fseek(f, 0, SEEK_END);
42 long n = ftell(f);
43 fseek(f, 0, SEEK_SET);
44 unsigned char *buf = malloc(n + 1);
45 if (fread(buf, 1, n, f) != (size_t)n) { fclose(f); free(buf); return NULL; }
46 fclose(f);
47 buf[n] = 0;
48 if (len) *len = n;
49 return buf;
50}
51
52int plat_write_file(const char *path, const void *data, size_t len)
53{
54 FILE *f = fopen(path, "wb");
55 if (!f) return 0;
56 int ok = fwrite(data, 1, len, f) == len;
57 return (fclose(f) == 0) && ok;
58}
59
60long long plat_file_size(const char *path)
61{
62 struct stat st;
63 return stat(path, &st) == 0 && S_ISREG(st.st_mode) ? (long long)st.st_size : -1;
64}
65
66int plat_dir_exists(const char *path)
67{
68 struct stat st;
69 return stat(path, &st) == 0 && S_ISDIR(st.st_mode);
70}
71
72int plat_mkdirs(const char *path)
73{
74 char buf[512];
75 snprintf(buf, sizeof(buf), "%s", path);
76 for (char *p = strchr(buf + 1, '/'); p; p = strchr(p + 1, '/')) { *p = 0; mkdir(buf, 0777); *p = '/'; }
77 mkdir(buf, 0777);
78 return plat_dir_exists(buf);
79}
80
81long long plat_free_bytes(const char *path)
82{
83 struct statvfs v;
84 return statvfs(path, &v) == 0 ? (long long)v.f_bavail * (long long)v.f_frsize : -1;
85}
86
87int plat_list_dirs(const char *path, char (*names)[64], int max)
88{
89 DIR *d = opendir(path);
90 struct dirent *e;
91 int n = 0;
92 if (!d) return 0;
93 while (n < max && (e = readdir(d)))
94 if (e->d_name[0] != '.' && e->d_type == DT_DIR) snprintf(names[n++], 64, "%s", e->d_name);
95 closedir(d);
96 return n;
97}
98
99void plat_log(const char *fmt, ...)
100{
101 va_list ap;
102 va_start(ap, fmt);
103 printf(" log: ");
104 vprintf(fmt, ap);
105 printf("\n");
106 va_end(ap);
107}
108
109struct plat_mutex { pthread_mutex_t m; };
110plat_mutex_t *plat_mutex_new(void)
111{
112 plat_mutex_t *m = malloc(sizeof(*m));
113 pthread_mutex_init(&m->m, NULL);
114 return m;
115}
116void plat_mutex_lock(plat_mutex_t *m) { pthread_mutex_lock(&m->m); }
117void plat_mutex_unlock(plat_mutex_t *m) { pthread_mutex_unlock(&m->m); }
118
119#define FILE_URL "https://dl.ps3.world/homebrewgames/Avoidance%20v1.3.pkg"
120#define FILE_SIZE 285360
121#define FILE_SHA "4df8da76f006b61f5755806d2497e02bdf4ca25738b5d0610cc6f51fab3c1511"
122#define RESUME_AT 100000
123
124static int failures;
125static char summary[16384];
126
127static void Result(int ok, const char *name, const char *fmt, ...)
128{
129 char msg[600];
130 va_list ap;
131 va_start(ap, fmt);
132 vsnprintf(msg, sizeof(msg), fmt, ap);
133 va_end(ap);
134 printf("%s %s: %s\n", ok ? "PASS" : "FAIL", name, msg);
135 // GitHub shows 10 annotations a step: failures each get one, the passes share one at the end
136 if (getenv("GITHUB_ACTIONS") && !ok)
137 printf("::error title=FAIL %s::%s\n", name, msg);
138 size_t n = strlen(summary);
139 snprintf(summary + n, sizeof(summary) - n, "%s %s: %s%%0A", ok ? "PASS" : "FAIL", name, msg);
140 if (!ok) failures++;
141}
142
143// ---- download.c ----
144static const unsigned char *Want_Sha(void)
145{
146 static unsigned char d[32];
147 for (int i = 0; i < 32; i++) sscanf(FILE_SHA + i * 2, "%2hhx", &d[i]);
148 return d;
149}
150
151static void Dl_Setup(download_t *d, const char *url, const char *dest, long long size)
152{
153 memset(d, 0, sizeof(*d));
154 snprintf(d->url, sizeof(d->url), "%s", url);
155 snprintf(d->dest, sizeof(d->dest), "%s", dest);
156 d->size = size;
157 memcpy(d->sha256, Want_Sha(), 32);
158}
159
160// stops a download (pause or cancel) once this much has arrived
161typedef struct { download_t *d; long long at; int cancel; } stopper_t;
162static void *Stopper(void *p)
163{
164 stopper_t *s = p;
165 while (s->d->state != DL_DONE && s->d->state != DL_FAILED && s->d->got < s->at) usleep(1000);
166 if (s->cancel) s->d->cancel = 1; else s->d->pause = 1;
167 return NULL;
168}
169
170static void Download_Tests(void)
171{
172 download_t d;
173 char part[600];
174 const char *dir = "/tmp/ps3world-dltest";
175 char dest[256];
176 plat_mkdirs(dir);
177 snprintf(dest, sizeof(dest), "%s/Avoidance v1.3.pkg", dir);
178 snprintf(part, sizeof(part), "%s.part", dest);
179 remove(dest);
180 remove(part);
181
182 // whole file
183 Dl_Setup(&d, FILE_URL, dest, FILE_SIZE);
184 download_run(&d);
185 Result(d.state == DL_DONE && plat_file_size(dest) == FILE_SIZE && plat_file_size(part) < 0, "download: whole file",
186 "state %d, %lld bytes on disk, %s", d.state, plat_file_size(dest), d.err);
187 remove(dest);
188
189 // stopped part way (as the network dropping or the app quitting), then resumed
190 pthread_t th;
191 stopper_t st = {&d, 120000, 0};
192 Dl_Setup(&d, FILE_URL, dest, FILE_SIZE);
193 pthread_create(&th, NULL, Stopper, &st);
194 download_run(&d);
195 pthread_join(th, NULL);
196 long long left = plat_file_size(part);
197 int st1 = d.state;
198 Dl_Setup(&d, FILE_URL, dest, FILE_SIZE);
199 download_run(&d);
200 Result(st1 == DL_PAUSED && left > 0 && d.state == DL_DONE && d.resumed_from == left && plat_file_size(dest) == FILE_SIZE,
201 "download: resume with Range", "stopped at %lld bytes (state %d), resumed from %lld, then state %d, %lld bytes, sha256 checked; %s",
202 left, st1, d.resumed_from, d.state, plat_file_size(dest), d.err);
203 remove(dest);
204
205 // a wrong sha256: refused and deleted
206 Dl_Setup(&d, FILE_URL, dest, FILE_SIZE);
207 d.sha256[0] ^= 0xff;
208 download_run(&d);
209 Result(d.state == DL_FAILED && d.err_kind == ERR_CHECKSUM && plat_file_size(dest) < 0 && plat_file_size(part) < 0,
210 "download: checksum mismatch deleted", "state %d, kind %d: %s", d.state, d.err_kind, d.err);
211
212 // more than the drive has
213 Dl_Setup(&d, FILE_URL, dest, 1LL << 50);
214 download_run(&d);
215 Result(d.state == DL_FAILED && d.err_kind == ERR_DISK_FULL, "download: not enough space", "kind %d: %s", d.err_kind, d.err);
216
217 // 404
218 Dl_Setup(&d, "https://dl.ps3.world/homebrewgames/no-such-file.pkg", dest, 1000);
219 download_run(&d);
220 Result(d.state == DL_FAILED && d.err_kind == ERR_NOT_FOUND && plat_file_size(part) < 0, "download: 404",
221 "kind %d: %s", d.err_kind, d.err);
222
223 // cancel: what arrived is deleted
224 stopper_t sc = {&d, 50000, 1};
225 Dl_Setup(&d, FILE_URL, dest, FILE_SIZE);
226 pthread_create(&th, NULL, Stopper, &sc);
227 download_run(&d);
228 pthread_join(th, NULL);
229 Result(d.state == DL_CANCELLED && plat_file_size(part) < 0 && plat_file_size(dest) < 0, "download: cancel",
230 "state %d, partial %lld", d.state, plat_file_size(part));
231}
232
233// the download sink: hashes as it goes and counts, like the app's will (nothing kept in memory)
234typedef struct {
235 mbedtls_sha256_context sha;
236 long long got;
237} sink_t;
238
239static int Sink(void *ctx, const void *data, int len)
240{
241 sink_t *s = ctx;
242 mbedtls_sha256_update(&s->sha, data, len);
243 s->got += len;
244 return 1;
245}
246
247static void Hex(const unsigned char *d, char *out)
248{
249 for (int i = 0; i < 32; i++) sprintf(out + i * 2, "%02x", d[i]);
250}
251
252static int Get(const char *url, const char *headers, sink_t *s, http_res_t *res)
253{
254 http_req_t req;
255 memset(&req, 0, sizeof(req));
256 req.url = url;
257 req.headers = headers;
258 req.sink = Sink;
259 req.ctx = s;
260 req.timeout_s = 30;
261 req.total_s = 120;
262 return http_request(&req, res);
263}
264
265// the sample catalogue: every page of a list against its sha256, parsed; a folder's list (an avatar pack) too
266typedef struct {
267 const char *root;
268 int lists, folders, pages, items, want, bad; // (want: the lists' counts; "All" repeats the rest)
269 char err[160];
270} sample_t;
271
272static void Sample_List(sample_t *s, const cat_list_t *l)
273{
274 s->lists++;
275 s->want += l->count;
276 for (int p = 0; p < l->num_pages; p++) {
277 char path[600];
278 unsigned char d[32];
279 size_t plen;
280 snprintf(path, sizeof(path), "%s/%s", s->root, l->pages[p].url);
281 char *page = (char *)plat_read_file(path, &plen);
282 mbedtls_sha256((const unsigned char *)(page ? page : ""), page ? plen : 0, d, 0);
283 catalog_t *c = page && !memcmp(d, l->pages[p].sha256, 32) ? catalog_parse_page(page, plen, s->err, sizeof(s->err)) : NULL;
284 if (!c) s->bad++;
285 for (int i = 0; c && i < c->count; i++) {
286 cat_list_t f;
287 if (!c->items[i].list[0]) continue;
288 if (catalog_parse_list(c->items[i].list, &f)) {
289 s->folders++;
290 Sample_List(s, &f);
291 catalog_free_list(&f);
292 } else
293 s->bad++;
294 }
295 if (c) { s->pages++; s->items += c->count; }
296 catalog_free(c);
297 free(page);
298 }
299}
300
301int main(int argc, char **argv)
302{
303 http_res_t res;
304 sink_t s;
305 unsigned char d[32];
306 char hex[65];
307 http_init();
308
309 // 1. whole file
310 mbedtls_sha256_init(&s.sha);
311 mbedtls_sha256_starts(&s.sha, 0);
312 s.got = 0;
313 int ok = Get(FILE_URL, NULL, &s, &res);
314 mbedtls_sha256_finish(&s.sha, d);
315 Hex(d, hex);
316 Result(ok && res.status == 200 && s.got == FILE_SIZE && !strcmp(hex, FILE_SHA), "whole file",
317 "HTTP %d, %lld bytes, sha256 %s%s%s", res.status, s.got, hex, res.err[0] ? ", " : "", res.err);
318
319 // 2. resume: the first RESUME_AT bytes, then Range from there
320 mbedtls_sha256_init(&s.sha);
321 mbedtls_sha256_starts(&s.sha, 0);
322 s.got = 0;
323 char range[64];
324 snprintf(range, sizeof(range), "Range: bytes=0-%d\r\n", RESUME_AT - 1);
325 int ok1 = Get(FILE_URL, range, &s, &res);
326 int st1 = res.status;
327 snprintf(range, sizeof(range), "Range: bytes=%d-\r\n", RESUME_AT);
328 int ok2 = Get(FILE_URL, range, &s, &res);
329 mbedtls_sha256_finish(&s.sha, d);
330 Hex(d, hex);
331 Result(ok1 && ok2 && st1 == 206 && res.status == 206 && res.length == FILE_SIZE - RESUME_AT && s.got == FILE_SIZE &&
332 !strcmp(hex, FILE_SHA), "Range resume", "HTTP %d then %d (second part %lld bytes), together %lld bytes, sha256 %s",
333 st1, res.status, res.length, s.got, hex);
334
335 // 3. 404
336 mbedtls_sha256_init(&s.sha);
337 mbedtls_sha256_starts(&s.sha, 0);
338 ok = Get("https://dl.ps3.world/catalog/v1/no-such-file.json", NULL, &s, &res);
339 Result(ok && res.status == 404, "404", "returned %d, HTTP %d", ok, res.status);
340
341 // 4. a certificate that must be refused
342 ok = Get("https://expired.badssl.com/", NULL, &s, &res);
343 Result(!ok && strstr(res.err, "certificate"), "expired certificate refused", "returned %d: %s", ok, res.err);
344 mbedtls_sha256_free(&s.sha);
345
346 // 5. the catalogue: the live index (once uploaded) and one of its pages, then the sample
347 size_t len;
348 char err[160];
349 char *body = http_fetch(NULL, CATALOG_URL, NULL, NULL, 60, NULL, &len, &res);
350 if (body && res.status == 200) {
351 cat_index_t *ix = catalog_parse_index(body, len, err, sizeof(err));
352 if (ix && ix->num_lists[TYPE_THEME]) {
353 Result(1, "live index", "%lu bytes: %d homebrew in %d lists, %d themes in %d pages, %d avatars, generated %s",
354 (unsigned long)len, ix->count[TYPE_HOMEBREW], ix->num_lists[TYPE_HOMEBREW], ix->count[TYPE_THEME],
355 ix->lists[TYPE_THEME][0].num_pages, ix->count[TYPE_AVATAR], ix->generated);
356 const cat_file_t *pf = &ix->lists[TYPE_THEME][0].pages[0];
357 char url[300];
358 size_t plen;
359 snprintf(url, sizeof(url), "%s%s", CATALOG_BASE, pf->url);
360 char *page = http_fetch(NULL, url, NULL, NULL, 60, NULL, &plen, &res);
361 mbedtls_sha256((const unsigned char *)(page ? page : ""), page ? plen : 0, d, 0);
362 catalog_t *c = page ? catalog_parse_page(page, plen, err, sizeof(err)) : NULL;
363 Result(c && !memcmp(d, pf->sha256, 32), "live theme page 1", "%s: %lu bytes, sha256 %s the index, %d items, first \"%s\" %s",
364 pf->url, (unsigned long)plen, memcmp(d, pf->sha256, 32) ? "DIFFERS from" : "matches", c ? c->count : 0,
365 c && c->count ? c->items[0].title : "", err);
366 catalog_free(c);
367 free(page);
368 } else
369 Result(0, "live index", "%s", ix ? "no theme list" : err);
370 catalog_free_index(ix);
371 } else {
372 printf("SKIP live index: HTTP %d %s (not uploaded yet?)\n", res.status, res.err);
373 }
374 free(body);
375 if (argc > 1) {
376 // argv[1]: the sample's root (tests/sample), its index at catalog/v1/catalog.json
377 char path[600];
378 size_t n;
379 snprintf(path, sizeof(path), "%s/catalog/v1/catalog.json", argv[1]);
380 char *buf = (char *)plat_read_file(path, &n);
381 cat_index_t *ix = buf ? catalog_parse_index(buf, n, err, sizeof(err)) : NULL;
382 sample_t s = {argv[1]};
383 for (int t = 0; ix && t < NUM_TYPES; t++)
384 for (int l = 0; l < ix->num_lists[t]; l++)
385 Sample_List(&s, &ix->lists[t][l]);
386 Result(ix && !s.bad && s.folders && s.items == s.want, "sample catalogue parsed",
387 "%s: %d lists (%d of them folders), %d pages, %d items (the lists say %d; %d homebrew, %d themes, %d avatars), %d bad %s",
388 argv[1], s.lists, s.folders, s.pages, s.items, s.want, ix ? ix->count[0] : 0, ix ? ix->count[1] : 0,
389 ix ? ix->count[2] : 0, s.bad, s.err);
390 catalog_free_index(ix);
391 free(buf);
392 }
393
394 Download_Tests();
395
396 printf("\n%s (%d failure%s)\n", failures ? "FAILED" : "all passed", failures, failures == 1 ? "" : "s");
397 if (getenv("GITHUB_ACTIONS"))
398 printf("::notice title=HTTPS and download tests: %s::%s\n", failures ? "FAILED" : "all passed", summary);
399 return failures ? 1 : 0;
400}