/src/httrack/src/htssniff.c
Line | Count | Source |
1 | | /* ------------------------------------------------------------ */ |
2 | | /* |
3 | | HTTrack Website Copier, Offline Browser for Windows and Unix |
4 | | Copyright (C) 1998-2017 Xavier Roche and other contributors |
5 | | |
6 | | This program is free software: you can redistribute it and/or modify |
7 | | it under the terms of the GNU General Public License as published by |
8 | | the Free Software Foundation, either version 3 of the License, or |
9 | | (at your option) any later version. |
10 | | |
11 | | This program is distributed in the hope that it will be useful, |
12 | | but WITHOUT ANY WARRANTY; without even the implied warranty of |
13 | | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
14 | | GNU General Public License for more details. |
15 | | |
16 | | You should have received a copy of the GNU General Public License |
17 | | along with this program. If not, see <http://www.gnu.org/licenses/>. |
18 | | |
19 | | Important notes: |
20 | | |
21 | | - We hereby ask people using this source NOT to use it in purpose of grabbing |
22 | | emails addresses, or collecting any other private information on persons. |
23 | | This would disgrace our work, and spoil the many hours we spent on it. |
24 | | |
25 | | Please visit our Website: http://www.httrack.com |
26 | | */ |
27 | | |
28 | | /* ------------------------------------------------------------ */ |
29 | | /* File: MIME magic-byte consistency checks */ |
30 | | /* Author: Xavier Roche */ |
31 | | /* ------------------------------------------------------------ */ |
32 | | |
33 | | #include "htssniff.h" |
34 | | |
35 | | #include <string.h> |
36 | | |
37 | | #include "htslib.h" |
38 | | |
39 | | /* One magic rule: `len` bytes at `off` confirm `mime`. */ |
40 | | typedef struct sniff_magic { |
41 | | const char *mime; |
42 | | unsigned short off; |
43 | | unsigned char len; |
44 | | const char *bytes; |
45 | | } sniff_magic; |
46 | | |
47 | | /* Direction is mime -> magic (verify a claim, never classify); types with |
48 | | no reliable magic (plain text, css, js..) are deliberately absent. Patterns |
49 | | follow the WHATWG MIME Sniffing Standard tables where it defines them |
50 | | (https://mimesniff.spec.whatwg.org/); the rest covers httrack's wider MIME |
51 | | set. Spec-only types absent from our MIME tables (EOT, font/collection) |
52 | | are omitted as unreachable. */ |
53 | | static const sniff_magic sniff_table[] = { |
54 | | /* images */ |
55 | | {"image/jpeg", 0, 3, "\xff\xd8\xff"}, |
56 | | {"image/pipeg", 0, 3, "\xff\xd8\xff"}, |
57 | | {"image/pjpeg", 0, 3, "\xff\xd8\xff"}, |
58 | | {"image/png", 0, 8, "\x89PNG\r\n\x1a\n"}, |
59 | | {"image/gif", 0, 6, "GIF87a"}, |
60 | | {"image/gif", 0, 6, "GIF89a"}, |
61 | | {"image/bmp", 0, 2, "BM"}, |
62 | | {"image/tiff", 0, 4, "II*\0"}, |
63 | | {"image/tiff", 0, 4, "MM\0*"}, |
64 | | {"image/x-icon", 0, 4, "\0\0\1\0"}, |
65 | | {"image/x-icon", 0, 4, "\0\0\2\0"}, /* Windows cursor, per the spec */ |
66 | | {"image/x-portable-bitmap", 0, 2, "P1"}, |
67 | | {"image/x-portable-bitmap", 0, 2, "P4"}, |
68 | | {"image/x-portable-pixmap", 0, 2, "P3"}, |
69 | | {"image/x-portable-pixmap", 0, 2, "P6"}, |
70 | | {"image/x-xpixmap", 0, 9, "/* XPM */"}, |
71 | | {"image/x-xbitmap", 0, 7, "#define"}, |
72 | | {"image/x-rgb", 0, 2, "\x01\xda"}, |
73 | | {"image/x-cmu-raster", 0, 4, "\xf1\x00\x40\xbb"}, |
74 | | /* audio */ |
75 | | {"audio/mpeg", 0, 3, "ID3"}, |
76 | | {"audio/basic", 0, 4, ".snd"}, |
77 | | {"audio/mid", 0, 8, "MThd\0\0\0\6"}, |
78 | | {"audio/midi", 0, 8, "MThd\0\0\0\6"}, |
79 | | {"audio/x-pn-realaudio", 0, 4, ".ra\xfd"}, |
80 | | {"audio/x-pn-realaudio", 0, 4, ".RMF"}, |
81 | | {"audio/x-pn-realaudio-plugin", 0, 4, ".ra\xfd"}, |
82 | | {"audio/x-pn-realaudio-plugin", 0, 4, ".RMF"}, |
83 | | {"audio/flac", 0, 4, "fLaC"}, |
84 | | {"audio/aac", 0, 4, "ADIF"}, |
85 | | /* video */ |
86 | | {"video/mpeg", 0, 4, "\x00\x00\x01\xba"}, |
87 | | {"video/mpeg", 0, 4, "\x00\x00\x01\xb3"}, |
88 | | {"video/x-sgi-movie", 0, 4, "MOVI"}, |
89 | | /* archives / compression */ |
90 | | {"application/x-gzip", 0, 3, "\x1f\x8b\x08"}, |
91 | | {"multipart/x-gzip", 0, 3, "\x1f\x8b\x08"}, |
92 | | {"application/x-compressed", 0, 3, "\x1f\x8b\x08"}, |
93 | | {"application/x-compress", 0, 2, "\x1f\x9d"}, |
94 | | {"application/x-bzip2", 0, 3, "BZh"}, |
95 | | {"application/x-7z-compressed", 0, 6, "7z\xbc\xaf\x27\x1c"}, |
96 | | /* 6-byte prefix common to RAR4 (spec) and RAR5 */ |
97 | | {"application/x-rar-compressed", 0, 6, "Rar!\x1a\x07"}, |
98 | | {"application/zstd", 0, 4, "\x28\xb5\x2f\xfd"}, |
99 | | {"application/arj", 0, 2, "\x60\xea"}, |
100 | | {"application/x-cpio", 0, 6, "070701"}, |
101 | | {"application/x-cpio", 0, 6, "070707"}, |
102 | | {"application/x-cpio", 0, 2, "\xc7\x71"}, |
103 | | {"application/x-sv4cpio", 0, 6, "070701"}, |
104 | | {"application/x-sv4crc", 0, 6, "070702"}, |
105 | | {"application/x-stuffit", 0, 8, "StuffIt "}, |
106 | | {"application/x-stuffit", 0, 4, "SIT!"}, |
107 | | {"application/mac-binhex40", 0, 10, "(This file"}, |
108 | | /* documents */ |
109 | | {"application/pdf", 0, 5, "%PDF-"}, |
110 | | {"application/postscript", 0, 2, "%!"}, |
111 | | {"application/rtf", 0, 5, "{\\rtf"}, |
112 | | {"application/x-dvi", 0, 2, "\xf7\x02"}, |
113 | | {"application/x-hdf", 0, 4, "\x0e\x03\x13\x01"}, |
114 | | {"application/x-hdf", 0, 8, "\x89HDF\r\n\x1a\n"}, |
115 | | {"application/x-netcdf", 0, 4, "CDF\x01"}, |
116 | | {"application/x-netcdf", 0, 4, "CDF\x02"}, |
117 | | {"application/x-msaccess", 0, 19, "\0\1\0\0Standard Jet DB"}, |
118 | | /* fonts */ |
119 | | {"font/woff", 0, 4, "wOFF"}, |
120 | | {"font/woff2", 0, 4, "wOF2"}, |
121 | | {"font/ttf", 0, 4, "\0\1\0\0"}, |
122 | | {"font/ttf", 0, 4, "true"}, |
123 | | {"font/otf", 0, 4, "OTTO"}, |
124 | | /* misc */ |
125 | | {"application/x-shockwave-flash", 0, 3, "FWS"}, |
126 | | {"application/x-shockwave-flash", 0, 3, "CWS"}, |
127 | | {"application/x-shockwave-flash", 0, 3, "ZWS"}, |
128 | | {"application/futuresplash", 0, 3, "FWS"}, |
129 | | {"application/x-director", 0, 4, "RIFX"}, |
130 | | {"application/x-director", 0, 4, "XFIR"}, |
131 | | {"application/x-java-vm", 0, 4, "\xca\xfe\xba\xbe"}, |
132 | | {"application/wasm", 0, 4, "\0asm"}, |
133 | | {"application/x-msmetafile", 0, 4, "\xd7\xcd\xc6\x9a"}, |
134 | | {"application/x-msmetafile", 0, 4, "\x01\x00\x09\x00"}, |
135 | | {"application/x-x509-ca-cert", 0, 2, "\x30\x82"}, |
136 | | {"application/x-pkcs12", 0, 2, "\x30\x82"}, |
137 | | {"application/x-pkcs7-mime", 0, 2, "\x30\x82"}, |
138 | | {"application/x-pkcs7-signature", 0, 2, "\x30\x82"}, |
139 | | {"application/x-pkcs7-certificates", 0, 2, "\x30\x82"}, |
140 | | {"x-world/x-vrml", 0, 5, "#VRML"}, |
141 | | {"application/x-bittorrent", 0, 11, "d8:announce"}, |
142 | | {"drawing/x-dwf", 0, 4, "(DWF"}, |
143 | | {"application/acad", 0, 4, "AC10"}, |
144 | | {NULL, 0, 0, NULL}}; |
145 | | |
146 | | /* MIME families sharing a container magic */ |
147 | | static const char *const zip_mimes[] = { |
148 | | "application/zip", "application/x-zip-compressed", "multipart/x-zip", NULL}; |
149 | | static const char *const zip_mime_prefixes[] = { |
150 | | "application/vnd.openxmlformats-officedocument.", |
151 | | "application/vnd.oasis.opendocument.", NULL}; |
152 | | static const char *const ole_mimes[] = {"application/msword", |
153 | | "application/excel", |
154 | | "application/vnd.ms-excel", |
155 | | "application/powerpoint", |
156 | | "application/vnd.ms-powerpoint", |
157 | | "application/vnd.ms-project", |
158 | | "application/vnd.ms-works", |
159 | | "application/x-msmoney", |
160 | | "application/x-mspublisher", |
161 | | NULL}; |
162 | | static const char *const tar_mimes[] = { |
163 | | "application/x-tar", "application/x-ustar", "application/x-gtar", NULL}; |
164 | | static const char *const ogg_mimes[] = {"application/ogg", "audio/ogg", |
165 | | "video/ogg", "audio/opus", NULL}; |
166 | | static const char *const ebml_mimes[] = {"video/webm", "audio/webm", NULL}; |
167 | | /* ISO-BMFF, any 'ftyp' brand: containers overlap too much to split */ |
168 | | static const char *const bmff_mimes[] = {"video/mp4", "audio/mp4", |
169 | | "video/quicktime", NULL}; |
170 | | static const char *const avif_mimes[] = {"image/avif", NULL}; |
171 | | static const char *const heic_mimes[] = {"image/heic", NULL}; |
172 | | static const char *const asf_mimes[] = {"video/x-ms-asf", "video/x-ms-wmv", |
173 | | "video/x-la-asf", NULL}; |
174 | | static const char *const xml_mimes[] = {"application/xml", "text/xml", |
175 | | "image/svg+xml", "image/svg-xml", NULL}; |
176 | | static const char *const svg_mimes[] = {"image/svg+xml", "image/svg-xml", NULL}; |
177 | | static const char *const html_mimes[] = {"text/html", NULL}; |
178 | | static const char *const pem_mimes[] = { |
179 | | "application/x-x509-ca-cert", "application/x-pkcs7-certificates", |
180 | | "application/x-pkcs7-mime", "application/x-pkcs7-signature", NULL}; |
181 | | |
182 | 0 | static hts_boolean mime_in(const char *const *list, const char *mime) { |
183 | 0 | size_t i; |
184 | |
|
185 | 0 | for (i = 0; list[i] != NULL; i++) |
186 | 0 | if (strfield2(list[i], mime)) |
187 | 0 | return HTS_TRUE; |
188 | 0 | return HTS_FALSE; |
189 | 0 | } |
190 | | |
191 | 0 | static hts_boolean mime_in_prefix(const char *const *list, const char *mime) { |
192 | 0 | size_t i; |
193 | |
|
194 | 0 | for (i = 0; list[i] != NULL; i++) |
195 | 0 | if (strfield(mime, list[i])) |
196 | 0 | return HTS_TRUE; |
197 | 0 | return HTS_FALSE; |
198 | 0 | } |
199 | | |
200 | | static hts_boolean has_bytes(const unsigned char *d, size_t n, size_t off, |
201 | 0 | const char *bytes, size_t len) { |
202 | | /* overflow-safe: untrusted n alone on one side */ |
203 | 0 | return n >= off && len <= n - off && memcmp(d + off, bytes, len) == 0 |
204 | 0 | ? HTS_TRUE |
205 | 0 | : HTS_FALSE; |
206 | 0 | } |
207 | | |
208 | 0 | static unsigned char ascii_lower(unsigned char c) { |
209 | 0 | return c >= 'A' && c <= 'Z' ? (unsigned char) (c + 32) : c; |
210 | 0 | } |
211 | | |
212 | | /* Case-insensitive text prefix after an optional UTF-8 BOM and whitespace. */ |
213 | | static hts_boolean has_text_prefix(const unsigned char *d, size_t n, |
214 | 0 | const char *prefix) { |
215 | 0 | const size_t len = strlen(prefix); |
216 | 0 | size_t i, k; |
217 | |
|
218 | 0 | i = n >= 3 && memcmp(d, "\xef\xbb\xbf", 3) == 0 ? 3 : 0; |
219 | 0 | while (i < n && (d[i] == ' ' || d[i] == '\t' || d[i] == '\r' || d[i] == '\n')) |
220 | 0 | i++; |
221 | 0 | if (len > n - i) /* i <= n from the loop above */ |
222 | 0 | return HTS_FALSE; |
223 | 0 | for (k = 0; k < len; k++) |
224 | 0 | if (ascii_lower(d[i + k]) != ascii_lower((unsigned char) prefix[k])) |
225 | 0 | return HTS_FALSE; |
226 | 0 | return HTS_TRUE; |
227 | 0 | } |
228 | | |
229 | | typedef enum sniff_op { |
230 | | SNIFF_QUERY_KNOWN, /* is any rule defined for this MIME? */ |
231 | | SNIFF_QUERY_MATCH /* do the bytes confirm this MIME? */ |
232 | | } sniff_op; |
233 | | |
234 | | /* Single walk for both queries so the rule set can't drift apart. */ |
235 | | static hts_boolean sniff_eval(sniff_op op, const unsigned char *d, size_t n, |
236 | 0 | const char *mime) { |
237 | 0 | size_t i; |
238 | | |
239 | | /* KNOWN short-circuits; MATCH tests the magic */ |
240 | 0 | #define SNIFF_RULE(cond) \ |
241 | 0 | do { \ |
242 | 0 | if (op == SNIFF_QUERY_KNOWN) \ |
243 | 0 | return HTS_TRUE; \ |
244 | 0 | if (cond) \ |
245 | 0 | return HTS_TRUE; \ |
246 | 0 | } while (0) |
247 | |
|
248 | 0 | for (i = 0; sniff_table[i].mime != NULL; i++) { |
249 | 0 | if (strfield2(sniff_table[i].mime, mime)) { |
250 | 0 | SNIFF_RULE(has_bytes(d, n, sniff_table[i].off, sniff_table[i].bytes, |
251 | 0 | sniff_table[i].len)); |
252 | 0 | } |
253 | 0 | } |
254 | 0 | if (mime_in(zip_mimes, mime) || mime_in_prefix(zip_mime_prefixes, mime)) { |
255 | 0 | SNIFF_RULE(has_bytes(d, n, 0, "PK\3\4", 4) || |
256 | 0 | has_bytes(d, n, 0, "PK\5\6", 4)); |
257 | 0 | } |
258 | 0 | if (mime_in(ole_mimes, mime)) { |
259 | 0 | SNIFF_RULE(has_bytes(d, n, 0, "\xd0\xcf\x11\xe0\xa1\xb1\x1a\xe1", 8)); |
260 | 0 | } |
261 | 0 | if (mime_in(tar_mimes, mime)) { |
262 | 0 | SNIFF_RULE(has_bytes(d, n, 257, "ustar", 5)); |
263 | 0 | } |
264 | 0 | if (mime_in(ogg_mimes, mime)) { |
265 | 0 | SNIFF_RULE(has_bytes(d, n, 0, "OggS\0", 5)); |
266 | 0 | } |
267 | 0 | if (mime_in(ebml_mimes, mime)) { |
268 | 0 | SNIFF_RULE(has_bytes(d, n, 0, "\x1a\x45\xdf\xa3", 4)); |
269 | 0 | } |
270 | 0 | if (mime_in(bmff_mimes, mime)) { |
271 | 0 | SNIFF_RULE(has_bytes(d, n, 4, "ftyp", 4)); |
272 | 0 | } |
273 | 0 | if (mime_in(avif_mimes, mime)) { |
274 | 0 | SNIFF_RULE(has_bytes(d, n, 4, "ftypavif", 8) || |
275 | 0 | has_bytes(d, n, 4, "ftypavis", 8)); |
276 | 0 | } |
277 | 0 | if (mime_in(heic_mimes, mime)) { |
278 | 0 | SNIFF_RULE( |
279 | 0 | has_bytes(d, n, 4, "ftyphei", 7) || has_bytes(d, n, 4, "ftyphev", 7) || |
280 | 0 | has_bytes(d, n, 4, "ftypmif1", 8) || has_bytes(d, n, 4, "ftypmsf1", 8)); |
281 | 0 | } |
282 | 0 | if (mime_in(asf_mimes, mime)) { |
283 | 0 | SNIFF_RULE(has_bytes(d, n, 0, "\x30\x26\xb2\x75\x8e\x66\xcf\x11", 8)); |
284 | 0 | } |
285 | 0 | if (strfield2("audio/x-wav", mime)) { |
286 | 0 | SNIFF_RULE(has_bytes(d, n, 0, "RIFF", 4) && has_bytes(d, n, 8, "WAVE", 4)); |
287 | 0 | } |
288 | 0 | if (strfield2("video/x-msvideo", mime)) { |
289 | 0 | SNIFF_RULE(has_bytes(d, n, 0, "RIFF", 4) && has_bytes(d, n, 8, "AVI ", 4)); |
290 | 0 | } |
291 | 0 | if (strfield2("image/webp", mime)) { |
292 | 0 | SNIFF_RULE(has_bytes(d, n, 0, "RIFF", 4) && |
293 | 0 | has_bytes(d, n, 8, "WEBPVP", 6)); |
294 | 0 | } |
295 | 0 | if (strfield2("image/x-portable-anymap", mime)) { |
296 | 0 | SNIFF_RULE(n >= 2 && d[0] == 'P' && d[1] >= '1' && d[1] <= '6'); |
297 | 0 | } |
298 | 0 | if (strfield2("audio/x-aiff", mime)) { |
299 | 0 | SNIFF_RULE( |
300 | 0 | has_bytes(d, n, 0, "FORM", 4) && |
301 | 0 | (has_bytes(d, n, 8, "AIFF", 4) || has_bytes(d, n, 8, "AIFC", 4))); |
302 | 0 | } |
303 | 0 | if (strfield2("audio/mpeg", mime)) { |
304 | | /* MPEG audio frame sync (11 bits), valid layer and bitrate fields */ |
305 | 0 | SNIFF_RULE(n >= 2 && d[0] == 0xff && (d[1] & 0xe0) == 0xe0 && |
306 | 0 | (d[1] & 0x06) != 0); |
307 | 0 | } |
308 | 0 | if (strfield2("audio/aac", mime)) { |
309 | | /* ADTS sync */ |
310 | 0 | SNIFF_RULE(n >= 2 && d[0] == 0xff && (d[1] & 0xf6) == 0xf0); |
311 | 0 | } |
312 | 0 | if (strfield2("video/mp2t", mime)) { |
313 | 0 | SNIFF_RULE(n >= 1 && d[0] == 0x47 && (n <= 188 || d[188] == 0x47)); |
314 | 0 | } |
315 | 0 | if (mime_in(xml_mimes, mime)) { |
316 | 0 | SNIFF_RULE(has_text_prefix(d, n, "<?xml")); |
317 | 0 | } |
318 | 0 | if (mime_in(svg_mimes, mime)) { |
319 | 0 | SNIFF_RULE(has_text_prefix(d, n, "<svg") || |
320 | 0 | has_text_prefix(d, n, "<!DOCTYPE svg")); |
321 | 0 | } |
322 | 0 | if (mime_in(html_mimes, mime)) { |
323 | 0 | SNIFF_RULE(has_text_prefix(d, n, "<!DOCTYPE") || |
324 | 0 | has_text_prefix(d, n, "<html") || |
325 | 0 | has_text_prefix(d, n, "<head")); |
326 | 0 | } |
327 | 0 | if (mime_in(pem_mimes, mime)) { |
328 | 0 | SNIFF_RULE(has_text_prefix(d, n, "-----BEGIN")); |
329 | 0 | } |
330 | 0 | if (strfield2("audio/x-mpegurl", mime)) { |
331 | 0 | SNIFF_RULE(has_text_prefix(d, n, "#EXTM3U")); |
332 | 0 | } |
333 | 0 | if (strfield2("text/x-vcard", mime)) { |
334 | 0 | SNIFF_RULE(has_text_prefix(d, n, "BEGIN:VCARD")); |
335 | 0 | } |
336 | 0 | #undef SNIFF_RULE |
337 | 0 | return HTS_FALSE; |
338 | 0 | } |
339 | | |
340 | 0 | hts_boolean hts_sniff_mime_known(const char *mime) { |
341 | 0 | if (mime == NULL || *mime == '\0') |
342 | 0 | return HTS_FALSE; |
343 | 0 | return sniff_eval(SNIFF_QUERY_KNOWN, NULL, 0, mime); |
344 | 0 | } |
345 | | |
346 | | hts_boolean hts_sniff_mime_consistent(const void *data, size_t size, |
347 | 0 | const char *mime) { |
348 | 0 | if (data == NULL || size == 0 || mime == NULL || *mime == '\0') |
349 | 0 | return HTS_FALSE; |
350 | 0 | return sniff_eval(SNIFF_QUERY_MATCH, (const unsigned char *) data, size, |
351 | 0 | mime); |
352 | 0 | } |