262 lines7.9 KBC
1/*
2download.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
15typedef 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
27int 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
39static 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
46static 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
51static 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
58static 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
97static 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
116void 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
252stopped:
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
259out:
260 if (c.f) fclose(c.f);
261 mbedtls_sha256_free(&c.sha);
262}