| 1 | /* |
| 2 | download.c: see download.h. |
| 3 | */ |
| 4 | #include <stdio.h> |
| 5 | #include <stdlib.h> |
| 6 | #include <string.h> |
| 7 | #include "mbedtls/sha256.h" |
| 8 | #include "plat.h" |
| 9 | #include "download.h" |
| 10 | #include "lang.h" |
| 11 |
|
| 12 | #define FREE_MARGIN (1024 * 1024) // left free on the drive beyond the file itself |
| 13 | #define READ_CHUNK 65536 |
| 14 |
|
| 15 | typedef struct { |
| 16 | download_t *d; |
| 17 | FILE *f; |
| 18 | mbedtls_sha256_context sha; |
| 19 | http_res_t *res; |
| 20 | const char *part; |
| 21 | long long want_from; // the Range we asked for (0: the whole file) |
| 22 | int bad_range; // a 206 that isn't the rest of this file |
| 23 | int started; // the first piece of the body has been seen |
| 24 | int write_failed; |
| 25 | } ctx_t; |
| 26 |
|
| 27 | int download_error_kind(const char *e, int status) |
| 28 | { |
| 29 | if (status == 404) return ERR_NOT_FOUND; |
| 30 | if (status && (status < 200 || status >= 300)) return ERR_HTTP; |
| 31 | // http_ps3.c's messages |
| 32 | if (strstr(e, "certificate") || strstr(e, "Secure connection")) return ERR_TLS; |
| 33 | if (strstr(e, "network isn't available") || strstr(e, "Can't find") || strstr(e, "Can't connect") || |
| 34 | strstr(e, "No reply") || strstr(e, "cut short") || strstr(e, "Request to")) |
| 35 | return ERR_NETWORK; |
| 36 | return ERR_HTTP; |
| 37 | } |
| 38 |
|
| 39 | static void Fail(download_t *d, int kind, const char *fmt, const char *arg) |
| 40 | { |
| 41 | d->err_kind = kind; |
| 42 | snprintf(d->err, sizeof(d->err), tr(fmt), arg ? arg : ""); |
| 43 | d->state = DL_FAILED; |
| 44 | } |
| 45 |
|
| 46 | static void Hex(const unsigned char *in, char *out) |
| 47 | { |
| 48 | for (int i = 0; i < 32; i++) snprintf(out + i * 2, 3, "%02x", in[i]); |
| 49 | } |
| 50 |
|
| 51 | static void Remove_Partial(const char *part, const char *side) |
| 52 | { |
| 53 | remove(part); |
| 54 | remove(side); |
| 55 | } |
| 56 |
|
| 57 | // the body, a piece at a time: to disk and into the hash, nothing kept in memory |
| 58 | static int Sink(void *p, const void *data, int len) |
| 59 | { |
| 60 | ctx_t *c = p; |
| 61 | download_t *d = c->d; |
| 62 | if (d->cancel || d->pause) |
| 63 | return 0; |
| 64 | if (!c->started) { |
| 65 | c->started = 1; |
| 66 | if (c->res->status != 200 && c->res->status != 206) |
| 67 | return 0; // an error page: not part of the file |
| 68 | if (c->res->status == 206 && c->res->length >= 0 && c->res->length != d->size - c->want_from) { |
| 69 | c->bad_range = 1; |
| 70 | return 0; |
| 71 | } |
| 72 | if (c->want_from && c->res->status == 200) { |
| 73 | // the server sent the whole file instead of the rest: start again from 0 |
| 74 | plat_log("download: Range ignored (200): starting %s again", d->dest); |
| 75 | fclose(c->f); |
| 76 | c->f = fopen(c->part, "wb"); |
| 77 | mbedtls_sha256_free(&c->sha); |
| 78 | mbedtls_sha256_init(&c->sha); |
| 79 | mbedtls_sha256_starts(&c->sha, 0); |
| 80 | d->got = 0; |
| 81 | d->resumed_from = 0; |
| 82 | if (!c->f) { c->write_failed = 1; return 0; } |
| 83 | } |
| 84 | } |
| 85 | if (d->got + len > d->size) |
| 86 | return 0; // more than the catalogue says: it isn't this file |
| 87 | if (fwrite(data, 1, len, c->f) != (size_t)len) { |
| 88 | c->write_failed = 1; |
| 89 | return 0; |
| 90 | } |
| 91 | mbedtls_sha256_update(&c->sha, data, len); |
| 92 | d->got += len; |
| 93 | return 1; |
| 94 | } |
| 95 |
|
| 96 | // hashes what's in the partial file already; 0 if it couldn't be read |
| 97 | static int Hash_Existing(const char *part, long long n, mbedtls_sha256_context *sha, download_t *d) |
| 98 | { |
| 99 | FILE *f = fopen(part, "rb"); |
| 100 | unsigned char *buf = malloc(READ_CHUNK); |
| 101 | long long done = 0; |
| 102 | if (!f || !buf) { if (f) fclose(f); free(buf); return 0; } |
| 103 | while (done < n && !d->cancel && !d->pause) { |
| 104 | size_t want = n - done > READ_CHUNK ? READ_CHUNK : (size_t)(n - done); |
| 105 | size_t r = fread(buf, 1, want, f); |
| 106 | if (r == 0) break; |
| 107 | mbedtls_sha256_update(sha, buf, r); |
| 108 | done += r; |
| 109 | d->got = done; |
| 110 | } |
| 111 | fclose(f); |
| 112 | free(buf); |
| 113 | return done == n; |
| 114 | } |
| 115 |
|
| 116 | void download_run(download_t *d) |
| 117 | { |
| 118 | char part[540], side[560], want_hex[65], got_hex[65], range[64], dir[512]; |
| 119 | unsigned char digest[32]; |
| 120 | http_res_t res; |
| 121 | http_req_t req; |
| 122 | ctx_t c; |
| 123 |
|
| 124 | memset(&c, 0, sizeof(c)); |
| 125 | c.d = d; |
| 126 | c.res = &res; |
| 127 | c.part = part; |
| 128 | d->err_kind = ERR_NONE; |
| 129 | d->err[0] = 0; |
| 130 | d->got = 0; |
| 131 | d->resumed_from = 0; |
| 132 | snprintf(part, sizeof(part), "%s.part", d->dest); |
| 133 | snprintf(side, sizeof(side), "%s.part.sha", d->dest); |
| 134 | Hex(d->sha256, want_hex); |
| 135 | snprintf(dir, sizeof(dir), "%s", d->dest); |
| 136 | char *slash = strrchr(dir, '/'); |
| 137 | if (slash) *slash = 0; |
| 138 |
|
| 139 | // what's there from last time, if it's this same file |
| 140 | d->state = DL_CHECKING; |
| 141 | long long have = plat_file_size(part); |
| 142 | if (have > 0) { |
| 143 | char *s = (char *)plat_read_file(side, NULL); |
| 144 | int same = s && !strncmp(s, want_hex, 64); |
| 145 | free(s); |
| 146 | if (!same || have > d->size) { |
| 147 | plat_log("download: %s.part is from another version: starting again", d->dest); |
| 148 | Remove_Partial(part, side); |
| 149 | have = 0; |
| 150 | } |
| 151 | } else |
| 152 | have = 0; |
| 153 |
|
| 154 | // room for the rest (and a little more) |
| 155 | long long free_b = plat_free_bytes(dir); |
| 156 | if (free_b >= 0 && free_b < d->size - have + FREE_MARGIN) { |
| 157 | char need[96]; |
| 158 | snprintf(need, sizeof(need), "%.1f MB needed, %.1f MB free", (d->size - have) / 1048576.0, free_b / 1048576.0); |
| 159 | Fail(d, ERR_DISK_FULL, "Not enough free space (%s)", need); |
| 160 | return; |
| 161 | } |
| 162 |
|
| 163 | mbedtls_sha256_init(&c.sha); |
| 164 | mbedtls_sha256_starts(&c.sha, 0); |
| 165 | if (have && !Hash_Existing(part, have, &c.sha, d)) { |
| 166 | if (d->cancel || d->pause) goto stopped; |
| 167 | plat_log("download: %s.part couldn't be read: starting again", d->dest); |
| 168 | mbedtls_sha256_free(&c.sha); |
| 169 | mbedtls_sha256_init(&c.sha); |
| 170 | mbedtls_sha256_starts(&c.sha, 0); |
| 171 | Remove_Partial(part, side); |
| 172 | have = 0; |
| 173 | } |
| 174 | d->got = have; |
| 175 | d->resumed_from = have; |
| 176 |
|
| 177 | if (have < d->size) { |
| 178 | if (!plat_write_file(side, want_hex, 64) || !(c.f = fopen(part, have ? "ab" : "wb"))) { |
| 179 | Fail(d, ERR_WRITE, "Couldn't write to %s", dir); |
| 180 | goto out; |
| 181 | } |
| 182 | memset(&req, 0, sizeof(req)); |
| 183 | req.url = d->url; |
| 184 | req.cancel = &d->cancel; // (pause stops it through the sink) |
| 185 | req.timeout_s = 30; |
| 186 | req.sink = Sink; |
| 187 | req.ctx = &c; |
| 188 | if (have) { |
| 189 | snprintf(range, sizeof(range), "Range: bytes=%lld-\r\n", have); |
| 190 | req.headers = range; |
| 191 | c.want_from = have; |
| 192 | plat_log("download: resuming %s at %lld of %lld", d->dest, have, d->size); |
| 193 | } |
| 194 | d->state = DL_RUNNING; |
| 195 | int ok = http_request(&req, &res); |
| 196 | if (c.f && fclose(c.f) != 0) c.write_failed = 1; |
| 197 | c.f = NULL; |
| 198 |
|
| 199 | if (d->cancel || d->pause) goto stopped; |
| 200 | if (c.write_failed) { |
| 201 | long long fb = plat_free_bytes(dir); |
| 202 | if (fb >= 0 && fb < READ_CHUNK) Fail(d, ERR_DISK_FULL, "The drive is full%s", ""); |
| 203 | else Fail(d, ERR_WRITE, "Couldn't write to %s", dir); |
| 204 | goto out; |
| 205 | } |
| 206 | if (c.bad_range) { |
| 207 | Remove_Partial(part, side); |
| 208 | Fail(d, ERR_HTTP, "The server sent the wrong part of the file. Try again%s", ""); |
| 209 | goto out; |
| 210 | } |
| 211 | if (res.status == 404) { Remove_Partial(part, side); Fail(d, ERR_NOT_FOUND, "Not found on the server (404)%s", ""); goto out; } |
| 212 | if (res.status == 416) { |
| 213 | // the partial file is longer than the server's: not this file |
| 214 | Remove_Partial(part, side); |
| 215 | Fail(d, ERR_HTTP, "The server's copy is different (416). Try again%s", ""); |
| 216 | goto out; |
| 217 | } |
| 218 | if (!ok || (res.status != 200 && res.status != 206)) { |
| 219 | char e[180]; |
| 220 | if (res.status && res.status != 200 && res.status != 206) snprintf(e, sizeof(e), "HTTP %d", res.status); |
| 221 | else snprintf(e, sizeof(e), "%s", res.err[0] ? res.err : "Download cut short"); |
| 222 | Fail(d, download_error_kind(res.err, res.status), "%s", e); |
| 223 | goto out; // (the partial file stays: the next try resumes) |
| 224 | } |
| 225 | if (d->got != d->size) { |
| 226 | Fail(d, ERR_NETWORK, "Download cut short%s", ""); |
| 227 | goto out; |
| 228 | } |
| 229 | } |
| 230 |
|
| 231 | // the whole file: is it the one the catalogue means? |
| 232 | d->state = DL_VERIFYING; |
| 233 | mbedtls_sha256_finish(&c.sha, digest); |
| 234 | Hex(digest, got_hex); |
| 235 | long long on_disk = plat_file_size(part); |
| 236 | if (on_disk != d->size || memcmp(digest, d->sha256, 32)) { |
| 237 | plat_log("download: %s: checksum mismatch (%lld bytes, sha256 %s, wanted %s)", d->dest, on_disk, got_hex, want_hex); |
| 238 | Remove_Partial(part, side); |
| 239 | Fail(d, ERR_CHECKSUM, "Checksum mismatch: the file was deleted. Try again%s", ""); |
| 240 | goto out; |
| 241 | } |
| 242 | remove(d->dest); |
| 243 | if (rename(part, d->dest) != 0) { |
| 244 | Fail(d, ERR_WRITE, "Couldn't rename the finished file in %s", dir); |
| 245 | goto out; |
| 246 | } |
| 247 | remove(side); |
| 248 | plat_log("download: %s done, %lld bytes, sha256 %s", d->dest, d->size, got_hex); |
| 249 | d->state = DL_DONE; |
| 250 | goto out; |
| 251 |
|
| 252 | stopped: |
| 253 | if (c.f) { fclose(c.f); c.f = NULL; } |
| 254 | if (d->cancel) { |
| 255 | Remove_Partial(part, side); |
| 256 | d->state = DL_CANCELLED; |
| 257 | } else |
| 258 | d->state = DL_PAUSED; // the partial file stays for next time |
| 259 | out: |
| 260 | if (c.f) fclose(c.f); |
| 261 | mbedtls_sha256_free(&c.sha); |
| 262 | } |