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