/src/wget2/libwget/metalink.c
Line | Count | Source |
1 | | /* |
2 | | * Copyright (c) 2012 Tim Ruehsen |
3 | | * Copyright (c) 2015-2026 Free Software Foundation, Inc. |
4 | | * |
5 | | * This file is part of libwget. |
6 | | * |
7 | | * Libwget is free software: you can redistribute it and/or modify |
8 | | * it under the terms of the GNU Lesser General Public License as published by |
9 | | * the Free Software Foundation, either version 3 of the License, or |
10 | | * (at your option) any later version. |
11 | | * |
12 | | * Libwget is distributed in the hope that it will be useful, |
13 | | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
14 | | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
15 | | * GNU Lesser General Public License for more details. |
16 | | * |
17 | | * You should have received a copy of the GNU Lesser General Public License |
18 | | * along with libwget. If not, see <https://www.gnu.org/licenses/>. |
19 | | * |
20 | | * |
21 | | * Metalink parsing routines |
22 | | * |
23 | | * Changelog |
24 | | * 10.07.2012 Tim Ruehsen created (refactored from wget.c) |
25 | | * |
26 | | * Resources: |
27 | | * RFC 5854 - The Metalink Download Description Format |
28 | | * RFC 6249 Metalink/HTTP: Mirrors and Hashes |
29 | | * RFC 5988 Link HTTP Header update |
30 | | * RFC 3864 Link HTTP Header |
31 | | * RFC 3230 Digest HTTP Header |
32 | | * |
33 | | * Some examples to test: |
34 | | * http://go-oo.mirrorbrain.org/stable/linux-x86/3.2.1/ooobasis3.2-af-calc-3.2.1-9505.i586.rpm |
35 | | * http://go-oo.mirrorbrain.org/stable/linux-x86/3.2.1/ooobasis3.2-ar-help-3.2.1-9505.i586.rpm |
36 | | * http://download.services.openoffice.org/files/stable/ |
37 | | * http://go-oo.mirrorbrain.org/evolution/stable/Evolution-2.24.0.exe |
38 | | */ |
39 | | |
40 | | #include <config.h> |
41 | | |
42 | | #include <stdio.h> |
43 | | #include <stdlib.h> |
44 | | #include <string.h> |
45 | | #include <limits.h> |
46 | | #include <xstrtol.h> |
47 | | |
48 | | #include <wget.h> |
49 | | #include "private.h" |
50 | | #include "filename.h" |
51 | | |
52 | | typedef struct { |
53 | | wget_metalink |
54 | | *metalink; |
55 | | int |
56 | | priority; |
57 | | // id; // counting piece number in metalink 3 |
58 | | char |
59 | | hash[128], |
60 | | hash_type[16], |
61 | | location[8]; |
62 | | long long |
63 | | length; |
64 | | } metalink_context ; |
65 | | |
66 | | static void mirror_free(void *mirror) |
67 | 31.0k | { |
68 | 31.0k | wget_metalink_mirror *m = mirror; |
69 | | |
70 | 31.0k | if (m) { |
71 | 31.0k | wget_iri_free((wget_iri **) &m->iri); |
72 | 31.0k | xfree(m); |
73 | 31.0k | } |
74 | 31.0k | } |
75 | | |
76 | | static void add_piece(metalink_context *ctx, const char *value) |
77 | 6.84k | { |
78 | 6.84k | wget_metalink *metalink = ctx->metalink; |
79 | | |
80 | 6.84k | sscanf(value, "%127s", ctx->hash); |
81 | | |
82 | 6.84k | if (ctx->length && *ctx->hash_type && *ctx->hash) { |
83 | | // hash for a piece of the file |
84 | 6.01k | wget_metalink_piece piece, *piecep; |
85 | | |
86 | 6.01k | if (!metalink->pieces) |
87 | 216 | metalink->pieces = wget_vector_create(32, NULL); |
88 | | |
89 | 6.01k | piece.length = ctx->length; |
90 | 6.01k | wget_strscpy(piece.hash.type, ctx->hash_type, sizeof(piece.hash.type)); |
91 | 6.01k | wget_strscpy(piece.hash.hash_hex, ctx->hash, sizeof(piece.hash.hash_hex)); |
92 | | |
93 | 6.01k | piecep = wget_vector_get(metalink->pieces, wget_vector_size(metalink->pieces) - 1); |
94 | 6.01k | if (piecep && piecep->length > 0) { |
95 | 1.44k | if (piecep->position <= LONG_MAX - piecep->length) |
96 | 1.20k | piece.position = piecep->position + piecep->length; |
97 | 247 | else |
98 | 247 | piece.position = 0; // integer overflow |
99 | 1.44k | } else |
100 | 4.56k | piece.position = 0; |
101 | 6.01k | wget_vector_add_memdup(metalink->pieces, &piece, sizeof(wget_metalink_piece)); |
102 | 6.01k | } |
103 | | |
104 | 6.84k | *ctx->hash = 0; |
105 | 6.84k | } |
106 | | |
107 | | static void add_file_hash(metalink_context *ctx, const char *value) |
108 | 822 | { |
109 | 822 | wget_metalink *metalink = ctx->metalink; |
110 | | |
111 | 822 | sscanf(value, "%127s", ctx->hash); |
112 | | |
113 | 822 | if (*ctx->hash_type && *ctx->hash) { |
114 | | // hashes for the complete file |
115 | 239 | wget_metalink_hash hash = { 0 }; |
116 | | |
117 | 239 | wget_strscpy(hash.type, ctx->hash_type, sizeof(hash.type)); |
118 | 239 | wget_strscpy(hash.hash_hex, ctx->hash, sizeof(hash.hash_hex)); |
119 | | |
120 | 239 | if (!metalink->hashes) |
121 | 29 | metalink->hashes = wget_vector_create(4, NULL); |
122 | 239 | wget_vector_add_memdup(metalink->hashes, &hash, sizeof(wget_metalink_hash)); |
123 | 239 | } |
124 | | |
125 | 822 | *ctx->hash_type = *ctx->hash = 0; |
126 | 822 | } |
127 | | |
128 | | static void add_mirror(metalink_context *ctx, const char *value) |
129 | 33.4k | { |
130 | 33.4k | wget_iri *iri = wget_iri_parse(value, NULL); |
131 | | |
132 | 33.4k | if (!iri) |
133 | 2.43k | return; |
134 | | |
135 | 31.0k | if (!wget_iri_supported(iri)) { |
136 | 0 | error_printf(_("Mirror scheme not supported: '%s'\n"), value); |
137 | 0 | wget_iri_free(&iri); |
138 | 0 | return; |
139 | 0 | } |
140 | | |
141 | | /* if (iri->scheme == WGET_IRI_SCHEME_HTTP) |
142 | | test_modify_hsts(iri); |
143 | | |
144 | | if (config.https_only && iri->scheme != WGET_IRI_SCHEME_HTTPS) { |
145 | | info_printf(_("Mirror '%s' dropped (https-only requested)\n"), value); |
146 | | wget_iri_free(&iri); |
147 | | return; |
148 | | } |
149 | | |
150 | | if (iri->scheme == WGET_IRI_SCHEME_HTTP && config.https_enforce) { |
151 | | wget_iri_set_scheme(iri, WGET_IRI_SCHEME_HTTPS); |
152 | | } |
153 | | */ |
154 | | |
155 | 31.0k | wget_metalink *metalink = ctx->metalink; |
156 | 31.0k | wget_metalink_mirror *mirror = wget_calloc(1, sizeof(wget_metalink_mirror)); |
157 | | |
158 | 31.0k | if (mirror) { |
159 | 31.0k | wget_strscpy(mirror->location, ctx->location, sizeof(mirror->location)); |
160 | 31.0k | mirror->priority = ctx->priority; |
161 | 31.0k | mirror->iri = iri; |
162 | | |
163 | 31.0k | if (!metalink->mirrors) { |
164 | 3.82k | metalink->mirrors = wget_vector_create(4, NULL); |
165 | 3.82k | wget_vector_set_destructor(metalink->mirrors, mirror_free); |
166 | 3.82k | } |
167 | 31.0k | wget_vector_add(metalink->mirrors, mirror); |
168 | 31.0k | } |
169 | | |
170 | 31.0k | *ctx->location = 0; |
171 | 31.0k | ctx->priority = 999999; |
172 | 31.0k | } |
173 | | |
174 | | static const char *sanitized_filename(const char *in) |
175 | 2.21k | { |
176 | | // RFC 5854: |
177 | | // The path MUST NOT contain any directory traversal |
178 | | // directives or information. The path MUST be relative. The path |
179 | | // MUST NOT begin with a "/", "./", or "../"; contain "/../"; or end |
180 | | // with "/..". |
181 | | // ISSLASH() takes care for '\\' on Windows. |
182 | 2.21k | const char *p = in + FILE_SYSTEM_PREFIX_LEN(in); // skip drive letter on Windows |
183 | | |
184 | | // Reject absolute paths |
185 | 2.21k | if (ISSLASH(*p)) |
186 | 385 | return NULL; |
187 | | |
188 | | // Reject "./", and "../" at the start |
189 | 1.83k | if ((p[0] == '.' && ISSLASH(p[1])) || (p[0] == '.' && p[1] == '.' && ISSLASH(p[2]))) |
190 | 518 | return NULL; |
191 | | |
192 | | // Reject trailing "/.." |
193 | 1.31k | size_t len = strlen(p); |
194 | 1.31k | if (len >= 3 && ISSLASH(p[len - 3]) && p[len - 2] == '.' && p[len - 1] == '.') |
195 | 409 | return NULL; |
196 | | |
197 | | // Reject "/../" anywhere in the path |
198 | 26.3k | for (; *p; p++) { |
199 | 26.2k | if (ISSLASH(p[0]) && p[1] == '.' && p[2] == '.' && ISSLASH(p[3])) |
200 | 745 | return NULL; |
201 | 26.2k | } |
202 | | |
203 | 161 | return wget_strdup(in); |
204 | 906 | } |
205 | | |
206 | | static void metalink_parse(void *context, int flags, const char *dir, const char *attr, const char *val, size_t len, size_t pos WGET_GCC_UNUSED) |
207 | 129k | { |
208 | 129k | metalink_context *ctx = context; |
209 | 129k | char valuebuf[1024]; |
210 | 129k | const char *value; |
211 | | |
212 | | // info_printf("\n%02X %s %s '%s'\n", flags, dir, attr, value); |
213 | 129k | if (!(flags & (XML_FLG_CONTENT | XML_FLG_ATTRIBUTE))) |
214 | 68.5k | return; // ignore comments |
215 | | |
216 | 61.2k | if (wget_strncasecmp_ascii(dir, "/metalink/file", 14)) |
217 | 5.79k | return; |
218 | | |
219 | 55.4k | dir += 14; |
220 | | |
221 | 55.4k | if (!(value = wget_strmemcpy_a(valuebuf, sizeof(valuebuf), val ? val : "", len))) |
222 | 0 | return; |
223 | | |
224 | 55.4k | if (!wget_strncasecmp_ascii(dir, "s/file", 6)) { |
225 | | // metalink 3 XML format |
226 | 4.89k | dir += 6; |
227 | | |
228 | 4.89k | if (attr) { |
229 | 3.67k | if (*dir == 0) { // /metalink/file |
230 | 656 | if (!ctx->metalink->name && !wget_strcasecmp_ascii(attr, "name")) { |
231 | 203 | ctx->metalink->name = sanitized_filename(value); |
232 | 203 | } |
233 | 3.01k | } else if (!wget_strcasecmp_ascii(dir, "/verification/pieces")) { |
234 | 1.41k | if (!wget_strcasecmp_ascii(attr, "type")) { |
235 | 199 | sscanf(value, "%15s", ctx->hash_type); |
236 | 1.21k | } else if (!wget_strcasecmp_ascii(attr, "length")) { |
237 | 855 | unsigned long long tmp_len; |
238 | 855 | if (xstrtoull(value, NULL, 10, &tmp_len, NULL) != LONGINT_OK) |
239 | 239 | tmp_len = 0; |
240 | 855 | ctx->length = (long long)tmp_len; |
241 | 855 | } |
242 | | // } else if (!wget_strcasecmp_ascii(dir, "/verification/pieces/hash")) { |
243 | | // if (!wget_strcasecmp_ascii(attr, "type")) { |
244 | | // ctx->id = atoi(value); |
245 | | // } |
246 | 1.60k | } else if (!wget_strcasecmp_ascii(dir, "/verification/hash")) { |
247 | 395 | if (!wget_strcasecmp_ascii(attr, "type")) { |
248 | 194 | sscanf(value, "%15s", ctx->hash_type); |
249 | 194 | } |
250 | 1.20k | } else if (!wget_strcasecmp_ascii(dir, "/resources/url")) { |
251 | 884 | if (!wget_strcasecmp_ascii(attr, "location")) { |
252 | 194 | sscanf(value, " %2[a-zA-Z]", ctx->location); // ISO 3166-1 alpha-2 two letter country code |
253 | | // } else if (!wget_strcasecmp_ascii(attr, "protocol")) { |
254 | | // sscanf(value, " %7[a-zA-Z]", ctx->protocol); // type of URL, e.g. HTTP, HTTPS, FTP, ... |
255 | | // } else if (!wget_strcasecmp_ascii(attr, "type")) { |
256 | | // sscanf(value, " %2[a-zA-Z]", ctx->type); // type of URL, e.g. HTTP, FTP, ... |
257 | 690 | } else if (!wget_strcasecmp_ascii(attr, "preference")) { |
258 | 436 | sscanf(value, " %6d", &ctx->priority); |
259 | 436 | if (ctx->priority < 1 || ctx->priority > 999999) |
260 | 213 | ctx->priority = 999999; |
261 | 436 | } |
262 | 884 | } |
263 | 3.67k | } else { |
264 | 1.22k | if (!wget_strcasecmp_ascii(dir, "/verification/pieces/hash")) { |
265 | 196 | add_piece(ctx, value); |
266 | 1.02k | } else if (!wget_strcasecmp_ascii(dir, "/verification/hash")) { |
267 | 194 | add_file_hash(ctx, value); |
268 | 832 | } else if (!wget_strcasecmp_ascii(dir, "/size")) { |
269 | 397 | unsigned long long tmp_size; |
270 | 397 | if (xstrtoull(value, NULL, 10, &tmp_size, NULL) != LONGINT_OK) |
271 | 200 | tmp_size = 0; |
272 | 397 | ctx->metalink->size = tmp_size; |
273 | 435 | } else if (!wget_strcasecmp_ascii(dir, "/resources/url")) { |
274 | 198 | add_mirror(ctx, value); |
275 | 198 | } |
276 | 1.22k | } |
277 | 50.5k | } else { |
278 | | // metalink 4 XML format |
279 | 50.5k | if (attr) { |
280 | 6.88k | if (*dir == 0) { // /metalink/file |
281 | 3.08k | if (!ctx->metalink->name && !wget_strcasecmp_ascii(attr, "name")) { |
282 | 2.01k | ctx->metalink->name = sanitized_filename(value); |
283 | 2.01k | } |
284 | 3.79k | } else if (!wget_strcasecmp_ascii(dir, "/pieces")) { |
285 | 1.35k | if (!wget_strcasecmp_ascii(attr, "type")) { |
286 | 417 | sscanf(value, "%15s", ctx->hash_type); |
287 | 937 | } else if (!wget_strcasecmp_ascii(attr, "length")) { |
288 | 676 | unsigned long long tmp_len; |
289 | 676 | if (xstrtoull(value, NULL, 10, &tmp_len, NULL) != LONGINT_OK) |
290 | 194 | tmp_len = 0; |
291 | 676 | ctx->length = (long long)tmp_len; |
292 | 676 | } |
293 | 2.44k | } else if (!wget_strcasecmp_ascii(dir, "/hash")) { |
294 | 880 | if (!wget_strcasecmp_ascii(attr, "type")) { |
295 | 627 | sscanf(value, "%15s", ctx->hash_type); |
296 | 627 | } |
297 | 1.56k | } else if (!wget_strcasecmp_ascii(dir, "/url")) { |
298 | 1.10k | if (!wget_strcasecmp_ascii(attr, "location")) { |
299 | 194 | sscanf(value, " %2[a-zA-Z]", ctx->location); // ISO 3166-1 alpha-2 two letter country code |
300 | 911 | } else if (!wget_strcasecmp_ascii(attr, "priority") || !wget_strcasecmp_ascii(attr, "preference")) { |
301 | 604 | sscanf(value, " %6d", &ctx->priority); |
302 | 604 | if (ctx->priority < 1 || ctx->priority > 999999) |
303 | 213 | ctx->priority = 999999; |
304 | 604 | } |
305 | 1.10k | } |
306 | 43.6k | } else { |
307 | 43.6k | if (!wget_strcasecmp_ascii(dir, "/pieces/hash")) { |
308 | 6.64k | add_piece(ctx, value); |
309 | 37.0k | } else if (!wget_strcasecmp_ascii(dir, "/hash")) { |
310 | 628 | add_file_hash(ctx, value); |
311 | 36.4k | } else if (!wget_strcasecmp_ascii(dir, "/size")) { |
312 | 2.60k | unsigned long long tmp_size; |
313 | 2.60k | if (xstrtoull(value, NULL, 10, &tmp_size, NULL) != LONGINT_OK) |
314 | 2.09k | tmp_size = 0; |
315 | 2.60k | ctx->metalink->size = tmp_size; |
316 | 33.8k | } else if (!wget_strcasecmp_ascii(dir, "/url")) { |
317 | 33.2k | add_mirror(ctx, value); |
318 | 33.2k | } |
319 | 43.6k | } |
320 | 50.5k | } |
321 | | |
322 | 55.4k | if (value != valuebuf) |
323 | 61 | xfree(value); |
324 | 55.4k | } |
325 | | |
326 | | wget_metalink *wget_metalink_parse(const char *xml) |
327 | 6.76k | { |
328 | 6.76k | if (!xml) |
329 | 0 | return NULL; |
330 | | |
331 | 6.76k | wget_metalink *metalink = wget_calloc(1, sizeof(wget_metalink)); |
332 | 6.76k | metalink_context ctx = { .metalink = metalink, .priority = 999999, .location = "-" }; |
333 | | |
334 | 6.76k | if (wget_xml_parse_buffer(xml, metalink_parse, &ctx, 0) != WGET_E_SUCCESS) { |
335 | 368 | error_printf(_("Error in parsing XML")); |
336 | 368 | wget_metalink_free(&metalink); |
337 | 368 | } |
338 | | |
339 | 6.76k | return metalink; |
340 | 6.76k | } |
341 | | |
342 | | void wget_metalink_free(wget_metalink **metalink) |
343 | 7.13k | { |
344 | 7.13k | if (metalink && *metalink) { |
345 | 6.76k | xfree((*metalink)->name); |
346 | 6.76k | wget_vector_free(&(*metalink)->mirrors); |
347 | 6.76k | wget_vector_free(&(*metalink)->hashes); |
348 | 6.76k | wget_vector_free(&(*metalink)->pieces); |
349 | 6.76k | xfree(*metalink); |
350 | 6.76k | } |
351 | 7.13k | } |
352 | | |
353 | | WGET_GCC_PURE |
354 | | static int compare_mirror(wget_metalink_mirror *m1, wget_metalink_mirror *m2) |
355 | 88.5k | { |
356 | 88.5k | return m1->priority - m2->priority; |
357 | 88.5k | } |
358 | | |
359 | | void wget_metalink_sort_mirrors(wget_metalink *metalink) |
360 | 6.76k | { |
361 | 6.76k | if (metalink) { |
362 | 6.39k | wget_vector_setcmpfunc(metalink->mirrors, (wget_vector_compare_fn *) compare_mirror); |
363 | 6.39k | wget_vector_sort(metalink->mirrors); |
364 | 6.39k | } |
365 | 6.76k | } |