git-svn gc: trigger incremental packing if "lotsa" single-object packs
[girocco.git] / src / rangecgi.c
blob0c4f3be1fc230b2e4e2b006ea6835b78bdcf7684
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 #include <sys/types.h>
72 #include <sys/uio.h>
73 #include <errno.h>
74 #include <fcntl.h>
75 #include <stdio.h>
76 #include <stdlib.h>
77 #include <string.h>
78 #include <sys/stat.h>
79 #include <time.h>
80 #include <unistd.h>
81 #ifdef __APPLE__
82 #include <AvailabilityMacros.h>
83 #ifndef MAC_OS_X_VERSION_10_5
84 #define MAC_OS_X_VERSION_10_5 1050
85 #endif
86 #if MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_5
87 typedef struct stat statrec;
88 #define fstatfunc(p,b) fstat(p,b)
89 #else
90 typedef struct stat64 statrec;
91 #define fstatfunc(p,b) fstat64(p,b)
92 #endif
93 #else
94 typedef struct stat statrec;
95 #define fstatfunc(p,b) fstat(p,b)
96 #endif
97 typedef unsigned long long bignum;
99 static void errorfail_(unsigned code, const char *status, const char *extrahdr)
101 (void)code;
102 (void)status;
103 (void)extrahdr;
104 exit(3);
107 static void errorexit_(unsigned code, const char *status, const char *extrahdr)
109 printf("Status: %u %s\r\n", code, status);
110 printf("%s\r\n", "Expires: Fri, 01 Jan 1980 00:00:00 GMT");
111 printf("%s\r\n", "Pragma: no-cache");
112 printf("%s\r\n", "Cache-Control: no-cache,max-age=0,must-revalidate");
113 printf("%s\r\n", "Accept-Ranges: bytes");
114 if (extrahdr)
115 printf("%s\r\n", extrahdr);
116 printf("%s\r\n", "Content-Type: text/plain; charset=utf-8; format=fixed");
117 printf("%s\r\n", "");
118 printf("%s\n", status);
119 fflush(stdout);
120 exit(0);
123 static void emithdrs(const char *ct, const char *fn, int exp, time_t lm,
124 const char *etag, bignum tl, int isr, bignum r1, bignum r2)
126 struct tm gt;
127 char dtstr[32];
128 const char *xtra = "";
130 if (isr)
131 if (isr > 0)
132 printf("Status: %u %s\r\n", 206, "Partial Content");
133 else
134 printf("Status: %u %s\r\n", 416, "Requested Range Not Satisfiable");
135 else
136 printf("Status: %u %s\r\n", 200, "OK");
137 if (exp > 0) {
138 time_t epsecs = time(NULL);
139 long esecs = 86400 * exp;
140 gt = *gmtime(&epsecs);
141 strftime(dtstr, sizeof(dtstr), "%a, %d %b %Y %H:%M:%S GMT", &gt);
142 printf("Date: %s\r\n", dtstr);
143 epsecs += esecs;
144 gt = *gmtime(&epsecs);
145 strftime(dtstr, sizeof(dtstr), "%a, %d %b %Y %H:%M:%S GMT", &gt);
146 printf("Expires: %s\r\n", dtstr);
147 printf("Cache-Control: public,max-age=%ld\r\n", esecs);
148 } else if (!exp) {
149 printf("%s\r\n", "Expires: Fri, 01 Jan 1980 00:00:00 GMT");
150 printf("%s\r\n", "Pragma: no-cache");
151 printf("%s\r\n", "Cache-Control: no-cache,max-age=0,must-revalidate");
153 printf("%s\r\n", "Accept-Ranges: bytes");
154 gt = *gmtime(&lm);
155 strftime(dtstr, sizeof(dtstr), "%a, %d %b %Y %H:%M:%S GMT", &gt);
156 printf("Last-Modified: %s\r\n", dtstr);
157 if (etag)
158 printf("ETag: %s\r\n", etag);
159 if (!isr) {
160 printf("Content-Length: %llu\r\n", tl);
161 } else if (isr > 0) {
162 printf("Content-Length: %llu\r\n", r2 - r1 + 1);
163 printf("Content-Range: bytes %llu-%llu/%llu\r\n", r1, r2, tl);
164 } else {
165 printf("Content-Range: bytes */%llu\r\n", tl);
167 if (isr >= 0) {
168 if (!ct || !*ct)
169 ct = "application/octet-stream";
170 printf("Content-Type: %s\r\n", ct);
171 if (fn && *fn)
172 printf("Content-Disposition: attachment; filename=\"%s\"\r\n", fn);
173 printf("%s\r\n", "Vary: Accept-Encoding");
174 } else {
175 printf("%s\r\n", "Content-Type: text/plain; charset=utf-8; format=fixed");
176 xtra = "Requested Range Not Satisfiable\n";
178 printf("\r\n%s", xtra);
181 static void error416(time_t lm, const char *etag, bignum tl)
183 emithdrs(NULL, NULL, -1, lm, etag, tl, -1, 0, 0);
184 fflush(stdout);
185 exit(0);
188 static void die(const char *msg)
190 fprintf(stderr, "%s\n", msg);
191 fflush(stderr);
192 exit(2);
195 static void readx(int fd, void *buf, size_t count)
197 char *buff = (char *)buf;
198 int err = 0;
199 while (count && (!err || err == EINTR || err == EAGAIN || err == EWOULDBLOCK)) {
200 ssize_t amt = read(fd, buff, count);
201 if (amt == -1) {
202 err = errno;
203 continue;
205 err = 0;
206 if (!amt)
207 break;
208 buff += (size_t)amt;
209 count -= (size_t)amt;
211 if (count) {
212 if (err)
213 die("failed reading file (error)");
214 else
215 die("failed reading file (EOF)");
219 static void writex(int fd, const void *buf, size_t count)
221 const char *buff = (const char *)buf;
222 int err = 0;
223 while (count && (!err || err == EINTR || err == EAGAIN || err == EWOULDBLOCK)) {
224 ssize_t amt = write(fd, buff, count);
225 if (amt == -1) {
226 err = errno;
227 continue;
229 err = 0;
230 if (!amt)
231 break;
232 buff += (size_t)amt;
233 count -= (size_t)amt;
235 if (count) {
236 if (err)
237 die("failed writing file (error)");
238 else
239 die("failed writing file (EOF)");
243 #define SIZEPWR 15
244 static char dumpbuff[1U << SIZEPWR];
245 void dumpfile(int fd, bignum start, bignum len)
247 off_t loc = lseek(fd, (off_t)start, SEEK_SET);
248 size_t maxread;
249 if (loc != (off_t)start)
250 die("lseek failed");
251 if (start & ((bignum)(sizeof(dumpbuff) - 1)))
252 maxread = sizeof(dumpbuff) - (size_t)(start & ((bignum)(sizeof(dumpbuff) - 1)));
253 else
254 maxread = sizeof(dumpbuff);
255 while (len) {
256 size_t cnt = len > (bignum)maxread ? maxread : (size_t)len;
257 readx(fd, dumpbuff, cnt);
258 writex(STDOUT_FILENO, dumpbuff, cnt);
259 len -= (bignum)cnt;
260 maxread = sizeof(dumpbuff);
263 #undef SIZEPWR
265 int main(int argc, char *argv[])
267 int isetag = 0;
268 void (*errorexit)(unsigned,const char *,const char *);
269 statrec f1, f2;
270 int e1, e2, i;
271 bignum l1, l2, tl;
272 bignum r1=0, r2=0;
273 bignum start, length;
274 time_t lm;
275 const char *rm = NULL;
276 const char *hr = NULL;
277 const char *hir = NULL;
278 const char *ct = NULL;
279 const char *fn = NULL;
280 int expdays = -1, opt_e = 0;
281 /* "inode_inode-size-time_t_micros" each in hex up to 8 bytes gives */
282 /* "16bytes_16bytes-16bytes-16bytes" plus NUL = 70 bytes (including "") */
283 char etag[70];
284 int fd1 = -1, fd2 = -1;
285 int mno = 0;
287 opterr = 0;
288 for (;;) {
289 int ch;
290 if (optind < argc && !strcmp(argv[optind], "--etag")) {
291 ch = -2;
292 ++optind;
293 } else {
294 ch = getopt(argc, argv, "c:e:f:m:");
296 if (ch == -1)
297 break;
298 switch (ch) {
299 case -2:
300 isetag = 1;
301 break;
302 case 'c':
303 ct = optarg;
304 break;
305 case 'f':
306 fn = optarg;
307 break;
308 case 'e':
310 int v, n;
311 if (sscanf(optarg, "%i%n", &v, &n) != 1 || n != (int)strlen(optarg))
312 exit(2);
313 expdays = v;
314 opt_e = 1;
315 break;
317 case 'm':
318 if (!optarg[0] || optarg[1])
319 exit(2);
320 if (optarg[0] != '0' && optarg[0] != '1' && optarg[0] != '2')
321 exit(2);
322 mno = optarg[0] - '0';
323 break;
324 default:
325 exit(2);
328 if (argc - optind != 2)
329 exit(2);
330 i = optind;
332 if (isetag) {
333 if (ct || fn || opt_e)
334 exit(2);
335 errorexit = errorfail_;
336 } else {
337 rm = getenv("REQUEST_METHOD");
338 if (!rm || !*rm)
339 exit(1);
340 hr = getenv("HTTP_RANGE");
341 if (hr)
342 hir = getenv("HTTP_IF_RANGE");
343 errorexit = errorexit_;
344 if (strcmp(rm, "GET") && strcmp(rm, "HEAD"))
345 errorexit(405, "Method Not Allowed", "Allow: GET,HEAD");
348 fd1 = open(argv[i], O_RDONLY);
349 e1 = fd1 >= 0 ? 0 : errno;
350 fd2 = open(argv[i+1], O_RDONLY);
351 e2 = fd2 >= 0 ? 0 : errno;
352 if (e1 == EACCES || e2 == EACCES)
353 errorexit(403, "Forbidden", NULL);
354 if (e1 == ENOENT || e1 == ENOTDIR || e2 == ENOENT || e2 == ENOTDIR)
355 errorexit(404, "Not Found", NULL);
356 e1 = fstatfunc(fd1, &f1) ? errno : 0;
357 e2 = fstatfunc(fd2, &f2) ? errno : 0;
358 if (e1 || e2)
359 errorexit(500, "Internal Server Error", NULL);
360 if (!S_ISREG(f1.st_mode) || !S_ISREG(f2.st_mode))
361 errorexit(500, "Internal Server Error", NULL);
362 if (mno == 1)
363 lm = f1.st_mtime;
364 else if (mno == 2)
365 lm = f2.st_mtime;
366 else if (f1.st_mtime >= f2.st_mtime)
367 lm = f1.st_mtime;
368 else
369 lm = f2.st_mtime;
370 l1 = f1.st_size;
371 l2 = f2.st_size;
372 tl = l1 + l2;
373 sprintf(etag, "\"%llx_%llx-%llx-%llx\"", (unsigned long long)f1.st_ino,
374 (unsigned long long)f2.st_ino, tl, (unsigned long long)lm * 1000000U);
376 if (isetag) {
377 close(fd2);
378 close(fd1);
379 printf("%s\n", etag);
380 exit(0);
383 if (hir && *hir && strcmp(etag, hir))
384 hr = NULL;
386 if (hr && !tl)
387 error416(lm, etag, tl); /* Range: not allowed on zero length content */
389 if (hr) {
390 /* Only one range may be specified and it must be bytes */
391 /* with a 2^64 value we could have "Range: bytes = 20-digit - 20-digit" */
392 int pos = -1;
393 int s = sscanf(hr, " %*[Bb]%*[Yy]%*[Tt]%*[Ee]%*[Ss] = %n", &pos);
394 if (s != 0 || pos < 6 || strchr(hr, ','))
395 errorexit(400, "Bad Request", NULL);
396 hr += pos;
397 if (*hr == '-') {
398 bignum trail;
399 ++hr;
400 /* It's a request for the trailing part */
401 if (strchr(hr, '-'))
402 errorexit(400, "Bad Request", NULL);
403 pos = -1;
404 s = sscanf(hr, " %llu%n", &trail, &pos);
405 if (s != 1 || pos < 1 || hr[pos])
406 errorexit(400, "Bad Request", NULL);
407 if (!trail || trail > tl)
408 error416(lm, etag, tl);
409 r1 = tl - trail;
410 r2 = tl - 1;
411 } else {
412 pos = -1;
413 s = sscanf(hr, "%llu - %n", &r1, &pos);
414 if (s != 1 || pos < 2)
415 errorexit(400, "Bad Request", NULL);
416 hr += pos;
417 if (*hr) {
418 if (*hr == '-')
419 errorexit(400, "Bad Request", NULL);
420 pos = -1;
421 s = sscanf(hr, "%llu %n", &r2, &pos);
422 if (s != 1 || pos < 1 || hr[pos])
423 errorexit(400, "Bad Request", NULL);
424 } else {
425 r2 = tl - 1;
427 if (r1 > r2 || r2 >= tl)
428 error416(lm, etag, tl);
430 start = r1;
431 length = r2 - r1 + 1;
432 } else {
433 start = 0;
434 length = tl;
437 emithdrs(ct, fn, expdays, lm, etag, tl, hr?1:0, r1, r2);
438 fflush(stdout);
440 if (strcmp(rm, "HEAD")) {
441 if (start < l1) {
442 bignum dl = l1 - start;
443 if (dl > length) dl = length;
444 dumpfile(fd1, start, dl);
445 start += dl;
446 length -= dl;
448 if (length && start >= l1) {
449 bignum dl;
450 start -= l1;
451 dl = l2 - start;
452 if (dl > length) dl = length;
453 dumpfile(fd2, start, dl);
457 close(fd2);
458 close(fd1);
459 return 0;