rangecgi.c: correct 416 error output
[girocco/mitgedanken.git] / src / rangecgi.c
blob8279021e500064da614c14b56157ec0f45d4c229
1 /*
3 rangecgi.c -- rangecgi utility to serve multiple files as one with range support
4 Copyright (C) 2014,2015,2016,2019 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>] [-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.
43 The default is "-m 0" which means use the latest mtime of the given files
44 when computing the ETag value. With "-m 1" always use the mtime from the
45 first file and with "-m 2" always use the mtime from the second file.
47 Other CGI parameters MUST be passed as environment variables in particular
48 REQUEST_METHOD MUST be set and to request a range, HTTP_RANGE MUST be set.
49 HTTP_IF_RANGE MAY be set. No other environment variables are examined.
51 Exit code 0 for CGI success (Status: header etc. output)
52 Exit code 1 for no REQUEST_METHOD.
53 Exit code 2 for file1 and/or file2 not given or wrong type or bad option.
54 Exit code 3 for --etag mode and file1 and/or file2 could not be opened.
56 If file1 and/or file2 is not found a 404 status will be output.
58 Normally a front end script will begin processing the initial CGI request
59 from the web server and then pass it on to this utility as appropriate.
61 If a "Range:" header is present and a non-empty "If-Range:" header is also
62 present then if the value in the "If-Range:" header does not exactly match
63 the computed "ETag:" value then the "Range:" header will be silently ignored.
66 #undef _FILE_OFFSET_BITS
67 #define _FILE_OFFSET_BITS 64
68 #include <sys/types.h>
69 #include <sys/uio.h>
70 #include <errno.h>
71 #include <fcntl.h>
72 #include <stdio.h>
73 #include <stdlib.h>
74 #include <string.h>
75 #include <sys/stat.h>
76 #include <time.h>
77 #include <unistd.h>
78 #ifdef __APPLE__
79 #include <AvailabilityMacros.h>
80 #ifndef MAC_OS_X_VERSION_10_5
81 #define MAC_OS_X_VERSION_10_5 1050
82 #endif
83 #if MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_5
84 typedef struct stat statrec;
85 #define fstatfunc(p,b) fstat(p,b)
86 #else
87 typedef struct stat64 statrec;
88 #define fstatfunc(p,b) fstat64(p,b)
89 #endif
90 #else
91 typedef struct stat statrec;
92 #define fstatfunc(p,b) fstat(p,b)
93 #endif
94 typedef unsigned long long bignum;
96 static void errorfail_(unsigned code, const char *status, const char *extrahdr)
98 (void)code;
99 (void)status;
100 (void)extrahdr;
101 exit(3);
104 static void errorexit_(unsigned code, const char *status, const char *extrahdr)
106 printf("Status: %u %s\r\n", code, status);
107 printf("%s\r\n", "Expires: Fri, 01 Jan 1980 00:00:00 GMT");
108 printf("%s\r\n", "Pragma: no-cache");
109 printf("%s\r\n", "Cache-Control: no-cache,max-age=0,must-revalidate");
110 printf("%s\r\n", "Accept-Ranges: bytes");
111 if (extrahdr)
112 printf("%s\r\n", extrahdr);
113 printf("%s\r\n", "Content-Type: text/plain; charset=utf-8; format=fixed");
114 printf("%s\r\n", "");
115 printf("%s\n", status);
116 fflush(stdout);
117 exit(0);
120 static void emithdrs(const char *ct, int exp, time_t lm, const char *etag,
121 bignum tl, int isr, bignum r1, bignum r2)
123 struct tm gt;
124 char dtstr[32];
125 const char *xtra = "";
127 if (isr)
128 if (isr > 0)
129 printf("Status: %u %s\r\n", 206, "Partial Content");
130 else
131 printf("Status: %u %s\r\n", 416, "Requested Range Not Satisfiable");
132 else
133 printf("Status: %u %s\r\n", 200, "OK");
134 if (exp > 0) {
135 time_t epsecs = time(NULL);
136 long esecs = 86400 * exp;
137 gt = *gmtime(&epsecs);
138 strftime(dtstr, sizeof(dtstr), "%a, %d %b %Y %H:%M:%S GMT", &gt);
139 printf("Date: %s\r\n", dtstr);
140 epsecs += esecs;
141 gt = *gmtime(&epsecs);
142 strftime(dtstr, sizeof(dtstr), "%a, %d %b %Y %H:%M:%S GMT", &gt);
143 printf("Expires: %s\r\n", dtstr);
144 printf("Cache-Control: public,max-age=%ld\r\n", esecs);
145 } else if (!exp) {
146 printf("%s\r\n", "Expires: Fri, 01 Jan 1980 00:00:00 GMT");
147 printf("%s\r\n", "Pragma: no-cache");
148 printf("%s\r\n", "Cache-Control: no-cache,max-age=0,must-revalidate");
150 printf("%s\r\n", "Accept-Ranges: bytes");
151 gt = *gmtime(&lm);
152 strftime(dtstr, sizeof(dtstr), "%a, %d %b %Y %H:%M:%S GMT", &gt);
153 printf("Last-Modified: %s\r\n", dtstr);
154 if (etag)
155 printf("ETag: %s\r\n", etag);
156 if (!isr) {
157 printf("Content-Length: %llu\r\n", tl);
158 } else if (isr > 0) {
159 printf("Content-Length: %llu\r\n", r2 - r1 + 1);
160 printf("Content-Range: bytes %llu-%llu/%llu\r\n", r1, r2, tl);
161 } else {
162 printf("Content-Range: bytes */%llu\r\n", tl);
164 if (isr >= 0) {
165 if (!ct || !*ct)
166 ct = "application/octet-stream";
167 printf("Content-Type: %s\r\n", ct);
168 printf("%s\r\n", "Vary: Accept-Encoding");
169 } else {
170 printf("%s\r\n", "Content-Type: text/plain; charset=utf-8; format=fixed");
171 xtra = "Requested Range Not Satisfiable\n";
173 printf("\r\n%s", xtra);
176 static void error416(time_t lm, const char *etag, bignum tl)
178 emithdrs(NULL, -1, lm, etag, tl, -1, 0, 0);
179 fflush(stdout);
180 exit(0);
183 static void die(const char *msg)
185 fprintf(stderr, "%s\n", msg);
186 fflush(stderr);
187 exit(2);
190 static void readx(int fd, void *buf, size_t count)
192 char *buff = (char *)buf;
193 int err = 0;
194 while (count && (!err || err == EINTR || err == EAGAIN || err == EWOULDBLOCK)) {
195 ssize_t amt = read(fd, buff, count);
196 if (amt == -1) {
197 err = errno;
198 continue;
200 err = 0;
201 if (!amt)
202 break;
203 buff += (size_t)amt;
204 count -= (size_t)amt;
206 if (count) {
207 if (err)
208 die("failed reading file (error)");
209 else
210 die("failed reading file (EOF)");
214 static void writex(int fd, const void *buf, size_t count)
216 const char *buff = (const char *)buf;
217 int err = 0;
218 while (count && (!err || err == EINTR || err == EAGAIN || err == EWOULDBLOCK)) {
219 ssize_t amt = write(fd, buff, count);
220 if (amt == -1) {
221 err = errno;
222 continue;
224 err = 0;
225 if (!amt)
226 break;
227 buff += (size_t)amt;
228 count -= (size_t)amt;
230 if (count) {
231 if (err)
232 die("failed writing file (error)");
233 else
234 die("failed writing file (EOF)");
238 #define SIZEPWR 15
239 static char dumpbuff[1U << SIZEPWR];
240 void dumpfile(int fd, bignum start, bignum len)
242 off_t loc = lseek(fd, (off_t)start, SEEK_SET);
243 size_t maxread;
244 if (loc != (off_t)start)
245 die("lseek failed");
246 if (start & ((bignum)(sizeof(dumpbuff) - 1)))
247 maxread = sizeof(dumpbuff) - (size_t)(start & ((bignum)(sizeof(dumpbuff) - 1)));
248 else
249 maxread = sizeof(dumpbuff);
250 while (len) {
251 size_t cnt = len > (bignum)maxread ? maxread : (size_t)len;
252 readx(fd, dumpbuff, cnt);
253 writex(STDOUT_FILENO, dumpbuff, cnt);
254 len -= (bignum)cnt;
255 maxread = sizeof(dumpbuff);
258 #undef SIZEPWR
260 int main(int argc, char *argv[])
262 int isetag = 0;
263 void (*errorexit)(unsigned,const char *,const char *);
264 statrec f1, f2;
265 int e1, e2, i;
266 bignum l1, l2, tl;
267 bignum r1=0, r2=0;
268 bignum start, length;
269 time_t lm;
270 const char *rm = NULL;
271 const char *hr = NULL;
272 const char *hir = NULL;
273 const char *ct = NULL;
274 int expdays = -1, opt_e = 0;
275 /* "inode_inode-size-time_t_micros" each in hex up to 8 bytes gives */
276 /* "16bytes_16bytes-16bytes-16bytes" plus NUL = 70 bytes (including "") */
277 char etag[70];
278 int fd1 = -1, fd2 = -1;
279 int mno = 0;
281 opterr = 0;
282 for (;;) {
283 int ch;
284 if (optind < argc && !strcmp(argv[optind], "--etag")) {
285 ch = -2;
286 ++optind;
287 } else {
288 ch = getopt(argc, argv, "c:e:m:");
290 if (ch == -1)
291 break;
292 switch (ch) {
293 case -2:
294 isetag = 1;
295 break;
296 case 'c':
297 ct = optarg;
298 break;
299 case 'e':
301 int v, n;
302 if (sscanf(optarg, "%i%n", &v, &n) != 1 || n != (int)strlen(optarg))
303 exit(2);
304 expdays = v;
305 opt_e = 1;
306 break;
308 case 'm':
309 if (!optarg[0] || optarg[1])
310 exit(2);
311 if (optarg[0] != '0' && optarg[0] != '1' && optarg[0] != '2')
312 exit(2);
313 mno = optarg[0] - '0';
314 break;
315 default:
316 exit(2);
319 if (argc - optind != 2)
320 exit(2);
321 i = optind;
323 if (isetag) {
324 if (ct || opt_e)
325 exit(2);
326 errorexit = errorfail_;
327 } else {
328 rm = getenv("REQUEST_METHOD");
329 if (!rm || !*rm)
330 exit(1);
331 hr = getenv("HTTP_RANGE");
332 if (hr)
333 hir = getenv("HTTP_IF_RANGE");
334 errorexit = errorexit_;
335 if (strcmp(rm, "GET") && strcmp(rm, "HEAD"))
336 errorexit(405, "Method Not Allowed", "Allow: GET,HEAD");
339 fd1 = open(argv[i], O_RDONLY);
340 e1 = fd1 >= 0 ? 0 : errno;
341 fd2 = open(argv[i+1], O_RDONLY);
342 e2 = fd2 >= 0 ? 0 : errno;
343 if (e1 == EACCES || e2 == EACCES)
344 errorexit(403, "Forbidden", NULL);
345 if (e1 == ENOENT || e1 == ENOTDIR || e2 == ENOENT || e2 == ENOTDIR)
346 errorexit(404, "Not Found", NULL);
347 e1 = fstatfunc(fd1, &f1) ? errno : 0;
348 e2 = fstatfunc(fd2, &f2) ? errno : 0;
349 if (e1 || e2)
350 errorexit(500, "Internal Server Error", NULL);
351 if (!S_ISREG(f1.st_mode) || !S_ISREG(f2.st_mode))
352 errorexit(500, "Internal Server Error", NULL);
353 if (mno == 1)
354 lm = f1.st_mtime;
355 else if (mno == 2)
356 lm = f2.st_mtime;
357 else if (f1.st_mtime >= f2.st_mtime)
358 lm = f1.st_mtime;
359 else
360 lm = f2.st_mtime;
361 l1 = f1.st_size;
362 l2 = f2.st_size;
363 tl = l1 + l2;
364 sprintf(etag, "\"%llx_%llx-%llx-%llx\"", (unsigned long long)f1.st_ino,
365 (unsigned long long)f2.st_ino, tl, (unsigned long long)lm * 1000000U);
367 if (isetag) {
368 close(fd2);
369 close(fd1);
370 printf("%s\n", etag);
371 exit(0);
374 if (hir && *hir && strcmp(etag, hir))
375 hr = NULL;
377 if (hr && !tl)
378 error416(lm, etag, tl); /* Range: not allowed on zero length content */
380 if (hr) {
381 /* Only one range may be specified and it must be bytes */
382 /* with a 2^64 value we could have "Range: bytes = 20-digit - 20-digit" */
383 int pos = -1;
384 int s = sscanf(hr, " %*[Bb]%*[Yy]%*[Tt]%*[Ee]%*[Ss] = %n", &pos);
385 if (s != 0 || pos < 6 || strchr(hr, ','))
386 errorexit(400, "Bad Request", NULL);
387 hr += pos;
388 if (*hr == '-') {
389 bignum trail;
390 ++hr;
391 /* It's a request for the trailing part */
392 if (strchr(hr, '-'))
393 errorexit(400, "Bad Request", NULL);
394 pos = -1;
395 s = sscanf(hr, " %llu%n", &trail, &pos);
396 if (s != 1 || pos < 1 || hr[pos])
397 errorexit(400, "Bad Request", NULL);
398 if (!trail || trail > tl)
399 error416(lm, etag, tl);
400 r1 = tl - trail;
401 r2 = tl - 1;
402 } else {
403 pos = -1;
404 s = sscanf(hr, "%llu - %n", &r1, &pos);
405 if (s != 1 || pos < 2)
406 errorexit(400, "Bad Request", NULL);
407 hr += pos;
408 if (*hr) {
409 if (*hr == '-')
410 errorexit(400, "Bad Request", NULL);
411 pos = -1;
412 s = sscanf(hr, "%llu %n", &r2, &pos);
413 if (s != 1 || pos < 1 || hr[pos])
414 errorexit(400, "Bad Request", NULL);
415 } else {
416 r2 = tl - 1;
418 if (r1 > r2 || r2 >= tl)
419 error416(lm, etag, tl);
421 start = r1;
422 length = r2 - r1 + 1;
423 } else {
424 start = 0;
425 length = tl;
428 emithdrs(ct, expdays, lm, etag, tl, hr?1:0, r1, r2);
429 fflush(stdout);
431 if (strcmp(rm, "HEAD")) {
432 if (start < l1) {
433 bignum dl = l1 - start;
434 if (dl > length) dl = length;
435 dumpfile(fd1, start, dl);
436 start += dl;
437 length -= dl;
439 if (length && start >= l1) {
440 bignum dl;
441 start -= l1;
442 dl = l2 - start;
443 if (dl > length) dl = length;
444 dumpfile(fd2, start, dl);
448 close(fd2);
449 close(fd1);
450 return 0;