html/volunteers: clean up some old style `/w` links
[girocco.git] / src / rangecgi.c
blob9450a64858bbef63e09e944cb106a2521577c618
1 /*
3 rangecgi.c -- rangecgi utility to serve multiple files as one with range support
4 Copyright (C) 2014,2015,2016,2019,2020 Kyle J. McKay
5 All rights reserved
7 This program is free software; you can redistribute it and/or
8 modify it under the terms of the GNU General Public License
9 as published by the Free Software Foundation; either version 2
10 of the License, or (at your option) any later version.
12 This program 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 General Public License for more details.
17 You should have received a copy of the GNU General Public License
18 along with this program; if not, write to the Free Software
19 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
24 This utility serves multiple files (currently exactly two) as though they
25 were one file and allows a continuation download using the "Range:" header.
27 Only GET and HEAD requests are supported with either no "Range:" header or
28 a "Range:" header with exactly one range.
30 USAGE:
31 rangecgi ([--etag] | [-c <content-type>] [-f <filename>] [-e <days>]) [-m 0|1|2] file1 file2
33 If --etag is given then all environment variables are ignored and the
34 computed ETag value (with the "", but without the "ETag:" prefix part) is
35 output to standard output on success. Otherwise there is no output and the
36 exit code will be non-zero.
38 If --etag is given then no other options except -m are allowed. If
39 -c <content-type> is given then the specified content type will be used as-is
40 for the returned item. If -e <days> is given then a cache-control and expires
41 header will be output with the expiration set that many days into the future.
42 If -f <filename> is given, then a "Content-Disposition: attachment;
43 filename=<filename>" header will be emitted. If no -f <filename> option is
44 given, then no "Content-Disposition:" header will be emitted!
46 The default is "-m 0" which means use the latest mtime of the given files
47 when computing the ETag value. With "-m 1" always use the mtime from the
48 first file and with "-m 2" always use the mtime from the second file.
50 Other CGI parameters MUST be passed as environment variables in particular
51 REQUEST_METHOD MUST be set and to request a range, HTTP_RANGE MUST be set.
52 HTTP_IF_RANGE MAY be set. No other environment variables are examined.
54 Exit code 0 for CGI success (Status: header etc. output)
55 Exit code 1 for no REQUEST_METHOD.
56 Exit code 2 for file1 and/or file2 not given or wrong type or bad option.
57 Exit code 3 for --etag mode and file1 and/or file2 could not be opened.
59 If file1 and/or file2 is not found a 404 status will be output.
61 Normally a front end script will begin processing the initial CGI request
62 from the web server and then pass it on to this utility as appropriate.
64 If a "Range:" header is present and a non-empty "If-Range:" header is also
65 present then if the value in the "If-Range:" header does not exactly match
66 the computed "ETag:" value then the "Range:" header will be silently ignored.
69 #undef _FILE_OFFSET_BITS
70 #define _FILE_OFFSET_BITS 64
71 #ifdef __APPLE__
72 #include <AvailabilityMacros.h>
73 #ifndef MAC_OS_X_VERSION_10_5
74 #define MAC_OS_X_VERSION_10_5 1050
75 #endif
76 #if MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5
77 #include <Availability.h>
78 #endif
79 #undef __OSX_AVAILABLE_BUT_DEPRECATED
80 #define __OSX_AVAILABLE_BUT_DEPRECATED(x,y,z,w)
81 #endif /* __APPLE__ */
82 #include <sys/types.h>
83 #include <sys/uio.h>
84 #include <errno.h>
85 #include <fcntl.h>
86 #include <stdio.h>
87 #include <stdlib.h>
88 #include <string.h>
89 #include <sys/stat.h>
90 #include <time.h>
91 #include <unistd.h>
92 #ifdef __APPLE__
93 #if MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_5
94 typedef struct stat statrec;
95 #define fstatfunc(p,b) fstat(p,b)
96 #else
97 typedef struct stat64 statrec;
98 #define fstatfunc(p,b) fstat64(p,b)
99 #endif
100 #else /* !__APPLE__ */
101 typedef struct stat statrec;
102 #define fstatfunc(p,b) fstat(p,b)
103 #endif /* !__APPLE__ */
104 typedef unsigned long long bignum;
106 static void errorfail_(unsigned code, const char *status, const char *extrahdr)
108 (void)code;
109 (void)status;
110 (void)extrahdr;
111 exit(3);
114 static void errorexit_(unsigned code, const char *status, const char *extrahdr)
116 printf("Status: %u %s\r\n", code, status);
117 printf("%s\r\n", "Expires: Fri, 01 Jan 1980 00:00:00 GMT");
118 printf("%s\r\n", "Pragma: no-cache");
119 printf("%s\r\n", "Cache-Control: no-cache,max-age=0,must-revalidate");
120 printf("%s\r\n", "Accept-Ranges: bytes");
121 if (extrahdr)
122 printf("%s\r\n", extrahdr);
123 printf("%s\r\n", "Content-Type: text/plain; charset=utf-8; format=fixed");
124 printf("%s\r\n", "");
125 printf("%s\n", status);
126 fflush(stdout);
127 exit(0);
130 static void emithdrs(const char *ct, const char *fn, int exp, time_t lm,
131 const char *etag, bignum tl, int isr, bignum r1, bignum r2)
133 struct tm gt;
134 char dtstr[32];
135 const char *xtra = "";
137 if (isr)
138 if (isr > 0)
139 printf("Status: %u %s\r\n", 206, "Partial Content");
140 else
141 printf("Status: %u %s\r\n", 416, "Requested Range Not Satisfiable");
142 else
143 printf("Status: %u %s\r\n", 200, "OK");
144 if (exp > 0) {
145 time_t epsecs = time(NULL);
146 long esecs = 86400 * exp;
147 gt = *gmtime(&epsecs);
148 strftime(dtstr, sizeof(dtstr), "%a, %d %b %Y %H:%M:%S GMT", &gt);
149 printf("Date: %s\r\n", dtstr);
150 epsecs += esecs;
151 gt = *gmtime(&epsecs);
152 strftime(dtstr, sizeof(dtstr), "%a, %d %b %Y %H:%M:%S GMT", &gt);
153 printf("Expires: %s\r\n", dtstr);
154 printf("Cache-Control: public,max-age=%ld\r\n", esecs);
155 } else if (!exp) {
156 printf("%s\r\n", "Expires: Fri, 01 Jan 1980 00:00:00 GMT");
157 printf("%s\r\n", "Pragma: no-cache");
158 printf("%s\r\n", "Cache-Control: no-cache,max-age=0,must-revalidate");
160 printf("%s\r\n", "Accept-Ranges: bytes");
161 gt = *gmtime(&lm);
162 strftime(dtstr, sizeof(dtstr), "%a, %d %b %Y %H:%M:%S GMT", &gt);
163 printf("Last-Modified: %s\r\n", dtstr);
164 if (etag)
165 printf("ETag: %s\r\n", etag);
166 if (!isr) {
167 printf("Content-Length: %llu\r\n", tl);
168 } else if (isr > 0) {
169 printf("Content-Length: %llu\r\n", r2 - r1 + 1);
170 printf("Content-Range: bytes %llu-%llu/%llu\r\n", r1, r2, tl);
171 } else {
172 printf("Content-Range: bytes */%llu\r\n", tl);
174 if (isr >= 0) {
175 if (!ct || !*ct)
176 ct = "application/octet-stream";
177 printf("Content-Type: %s\r\n", ct);
178 if (fn && *fn)
179 printf("Content-Disposition: attachment; filename=\"%s\"\r\n", fn);
180 printf("%s\r\n", "Vary: Accept-Encoding");
181 } else {
182 printf("%s\r\n", "Content-Type: text/plain; charset=utf-8; format=fixed");
183 xtra = "Requested Range Not Satisfiable\n";
185 printf("\r\n%s", xtra);
188 static void error416(time_t lm, const char *etag, bignum tl)
190 emithdrs(NULL, NULL, -1, lm, etag, tl, -1, 0, 0);
191 fflush(stdout);
192 exit(0);
195 static void die(const char *msg)
197 fprintf(stderr, "%s\n", msg);
198 fflush(stderr);
199 exit(2);
202 static void readx(int fd, void *buf, size_t count)
204 char *buff = (char *)buf;
205 int err = 0;
206 while (count && (!err || err == EINTR || err == EAGAIN || err == EWOULDBLOCK)) {
207 ssize_t amt = read(fd, buff, count);
208 if (amt == -1) {
209 err = errno;
210 continue;
212 err = 0;
213 if (!amt)
214 break;
215 buff += (size_t)amt;
216 count -= (size_t)amt;
218 if (count) {
219 if (err)
220 die("failed reading file (error)");
221 else
222 die("failed reading file (EOF)");
226 static void writex(int fd, const void *buf, size_t count)
228 const char *buff = (const char *)buf;
229 int err = 0;
230 while (count && (!err || err == EINTR || err == EAGAIN || err == EWOULDBLOCK)) {
231 ssize_t amt = write(fd, buff, count);
232 if (amt == -1) {
233 err = errno;
234 continue;
236 err = 0;
237 if (!amt)
238 break;
239 buff += (size_t)amt;
240 count -= (size_t)amt;
242 if (count) {
243 if (err)
244 die("failed writing file (error)");
245 else
246 die("failed writing file (EOF)");
250 #define SIZEPWR 15
251 static char dumpbuff[1U << SIZEPWR];
252 void dumpfile(int fd, bignum start, bignum len)
254 off_t loc = lseek(fd, (off_t)start, SEEK_SET);
255 size_t maxread;
256 if (loc != (off_t)start)
257 die("lseek failed");
258 if (start & ((bignum)(sizeof(dumpbuff) - 1)))
259 maxread = sizeof(dumpbuff) - (size_t)(start & ((bignum)(sizeof(dumpbuff) - 1)));
260 else
261 maxread = sizeof(dumpbuff);
262 while (len) {
263 size_t cnt = len > (bignum)maxread ? maxread : (size_t)len;
264 readx(fd, dumpbuff, cnt);
265 writex(STDOUT_FILENO, dumpbuff, cnt);
266 len -= (bignum)cnt;
267 maxread = sizeof(dumpbuff);
270 #undef SIZEPWR
272 int main(int argc, char *argv[])
274 int isetag = 0;
275 void (*errorexit)(unsigned,const char *,const char *);
276 statrec f1, f2;
277 int e1, e2, i;
278 bignum l1, l2, tl;
279 bignum r1=0, r2=0;
280 bignum start, length;
281 time_t lm;
282 const char *rm = NULL;
283 const char *hr = NULL;
284 const char *hir = NULL;
285 const char *ct = NULL;
286 const char *fn = NULL;
287 int expdays = -1, opt_e = 0;
288 /* "inode_inode-size-time_t_micros" each in hex up to 8 bytes gives */
289 /* "16bytes_16bytes-16bytes-16bytes" plus NUL = 70 bytes (including "") */
290 char etag[70];
291 int fd1 = -1, fd2 = -1;
292 int mno = 0;
294 opterr = 0;
295 for (;;) {
296 int ch;
297 if (optind < argc && !strcmp(argv[optind], "--etag")) {
298 ch = -2;
299 ++optind;
300 } else {
301 ch = getopt(argc, argv, "c:e:f:m:");
303 if (ch == -1)
304 break;
305 switch (ch) {
306 case -2:
307 isetag = 1;
308 break;
309 case 'c':
310 ct = optarg;
311 break;
312 case 'f':
313 fn = optarg;
314 break;
315 case 'e':
317 int v, n;
318 if (sscanf(optarg, "%i%n", &v, &n) != 1 || n != (int)strlen(optarg))
319 exit(2);
320 expdays = v;
321 opt_e = 1;
322 break;
324 case 'm':
325 if (!optarg[0] || optarg[1])
326 exit(2);
327 if (optarg[0] != '0' && optarg[0] != '1' && optarg[0] != '2')
328 exit(2);
329 mno = optarg[0] - '0';
330 break;
331 default:
332 exit(2);
335 if (argc - optind != 2)
336 exit(2);
337 i = optind;
339 if (isetag) {
340 if (ct || fn || opt_e)
341 exit(2);
342 errorexit = errorfail_;
343 } else {
344 rm = getenv("REQUEST_METHOD");
345 if (!rm || !*rm)
346 exit(1);
347 hr = getenv("HTTP_RANGE");
348 if (hr)
349 hir = getenv("HTTP_IF_RANGE");
350 errorexit = errorexit_;
351 if (strcmp(rm, "GET") && strcmp(rm, "HEAD"))
352 errorexit(405, "Method Not Allowed", "Allow: GET,HEAD");
355 fd1 = open(argv[i], O_RDONLY);
356 e1 = fd1 >= 0 ? 0 : errno;
357 fd2 = open(argv[i+1], O_RDONLY);
358 e2 = fd2 >= 0 ? 0 : errno;
359 if (e1 == EACCES || e2 == EACCES)
360 errorexit(403, "Forbidden", NULL);
361 if (e1 == ENOENT || e1 == ENOTDIR || e2 == ENOENT || e2 == ENOTDIR)
362 errorexit(404, "Not Found", NULL);
363 e1 = fstatfunc(fd1, &f1) ? errno : 0;
364 e2 = fstatfunc(fd2, &f2) ? errno : 0;
365 if (e1 || e2)
366 errorexit(500, "Internal Server Error", NULL);
367 if (!S_ISREG(f1.st_mode) || !S_ISREG(f2.st_mode))
368 errorexit(500, "Internal Server Error", NULL);
369 if (mno == 1)
370 lm = f1.st_mtime;
371 else if (mno == 2)
372 lm = f2.st_mtime;
373 else if (f1.st_mtime >= f2.st_mtime)
374 lm = f1.st_mtime;
375 else
376 lm = f2.st_mtime;
377 l1 = f1.st_size;
378 l2 = f2.st_size;
379 tl = l1 + l2;
380 sprintf(etag, "\"%llx_%llx-%llx-%llx\"", (unsigned long long)f1.st_ino,
381 (unsigned long long)f2.st_ino, tl, (unsigned long long)lm * 1000000U);
383 if (isetag) {
384 close(fd2);
385 close(fd1);
386 printf("%s\n", etag);
387 exit(0);
390 if (hir && *hir && strcmp(etag, hir))
391 hr = NULL;
393 if (hr && !tl)
394 error416(lm, etag, tl); /* Range: not allowed on zero length content */
396 if (hr) {
397 /* Only one range may be specified and it must be bytes */
398 /* with a 2^64 value we could have "Range: bytes = 20-digit - 20-digit" */
399 int pos = -1;
400 int s = sscanf(hr, " %*[Bb]%*[Yy]%*[Tt]%*[Ee]%*[Ss] = %n", &pos);
401 if (s != 0 || pos < 6 || strchr(hr, ','))
402 errorexit(400, "Bad Request", NULL);
403 hr += pos;
404 if (*hr == '-') {
405 bignum trail;
406 ++hr;
407 /* It's a request for the trailing part */
408 if (strchr(hr, '-'))
409 errorexit(400, "Bad Request", NULL);
410 pos = -1;
411 s = sscanf(hr, " %llu%n", &trail, &pos);
412 if (s != 1 || pos < 1 || hr[pos])
413 errorexit(400, "Bad Request", NULL);
414 if (!trail || trail > tl)
415 error416(lm, etag, tl);
416 r1 = tl - trail;
417 r2 = tl - 1;
418 } else {
419 pos = -1;
420 s = sscanf(hr, "%llu - %n", &r1, &pos);
421 if (s != 1 || pos < 2)
422 errorexit(400, "Bad Request", NULL);
423 hr += pos;
424 if (*hr) {
425 if (*hr == '-')
426 errorexit(400, "Bad Request", NULL);
427 pos = -1;
428 s = sscanf(hr, "%llu %n", &r2, &pos);
429 if (s != 1 || pos < 1 || hr[pos])
430 errorexit(400, "Bad Request", NULL);
431 } else {
432 r2 = tl - 1;
434 if (r1 > r2 || r2 >= tl)
435 error416(lm, etag, tl);
437 start = r1;
438 length = r2 - r1 + 1;
439 } else {
440 start = 0;
441 length = tl;
444 emithdrs(ct, fn, expdays, lm, etag, tl, hr?1:0, r1, r2);
445 fflush(stdout);
447 if (strcmp(rm, "HEAD")) {
448 if (start < l1) {
449 bignum dl = l1 - start;
450 if (dl > length) dl = length;
451 dumpfile(fd1, start, dl);
452 start += dl;
453 length -= dl;
455 if (length && start >= l1) {
456 bignum dl;
457 start -= l1;
458 dl = l2 - start;
459 if (dl > length) dl = length;
460 dumpfile(fd2, start, dl);
464 close(fd2);
465 close(fd1);
466 return 0;