htaccess: add note about making mod_status work
[girocco.git] / src / rangecgi.c
blob60c30a9cd7141f4760e43231b71d353a09c39127
1 /*
3 rangecgi.c -- rangecgi utility to serve multiple files as one with range support
4 Copyright (C) 2014,2015 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: rangecgi ([--etag] | [-c <content-type>] [-e <days>]) file1 file2
31 If --etag is given then all environment variables are ignored and the
32 computed ETag value (with the "", but without the "ETag:" prefix part) is
33 output to standard output on success. Otherwise there is no output and the
34 exit code will be non-zero.
36 If --etag is given then no other options are allowed. If -c <content-type>
37 is given then the specified content type will be used as-is for the returned
38 item. If -e <days> is given then a cache-control and expires header will
39 be output with the expiration set that many days into the future.
41 Other CGI parameters MUST be passed as environment variables in particular
42 REQUEST_METHOD MUST be set and to request a range, HTTP_RANGE MUST be set.
43 HTTP_IF_RANGE MAY be set. No other environment variables are examined.
45 Exit code 0 for CGI success (Status: header etc. output)
46 Exit code 1 for no REQUEST_METHOD.
47 Exit code 2 for file1 and/or file2 not given or wrong type or bad option.
48 Exit code 3 for --etag mode and file1 and/or file2 could not be opened.
50 If file1 and/or file2 is not found a 404 status will be output.
52 Normally a front end script will begin processing the initial CGI request
53 from the web server and then pass it on to this utility as appropriate.
55 If a "Range:" header is present and a non-empty "If-Range:" header is also
56 present then if the value in the "If-Range:" header does not exactly match
57 the computed "ETag:" value then the "Range:" header will be silently ignored.
60 #undef _FILE_OFFSET_BITS
61 #define _FILE_OFFSET_BITS 64
62 #include <sys/types.h>
63 #include <sys/uio.h>
64 #include <errno.h>
65 #include <fcntl.h>
66 #include <stdio.h>
67 #include <stdlib.h>
68 #include <string.h>
69 #include <sys/stat.h>
70 #include <time.h>
71 #include <unistd.h>
72 #ifdef __APPLE__
73 #include <AvailabilityMacros.h>
74 #ifndef MAC_OS_X_VERSION_10_5
75 #define MAC_OS_X_VERSION_10_5 1050
76 #endif
77 #if MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_5
78 typedef struct stat statrec;
79 #define fstatfunc(p,b) fstat(p,b)
80 #else
81 typedef struct stat64 statrec;
82 #define fstatfunc(p,b) fstat64(p,b)
83 #endif
84 #else
85 typedef struct stat statrec;
86 #define fstatfunc(p,b) fstat(p,b)
87 #endif
88 typedef unsigned long long bignum;
90 static void errorfail_(unsigned code, const char *status, const char *extrahdr)
92 (void)code;
93 (void)status;
94 (void)extrahdr;
95 exit(3);
98 static void errorexit_(unsigned code, const char *status, const char *extrahdr)
100 printf("Status: %u %s\r\n", code, status);
101 printf("%s\r\n", "Expires: Fri, 01 Jan 1980 00:00:00 GMT");
102 printf("%s\r\n", "Pragma: no-cache");
103 printf("%s\r\n", "Cache-Control: no-cache,max-age=0,must-revalidate");
104 printf("%s\r\n", "Accept-Ranges: bytes");
105 if (extrahdr)
106 printf("%s\r\n", extrahdr);
107 printf("%s\r\n", "Content-Type: text/plain");
108 printf("%s\r\n", "");
109 printf("%s\n", status);
110 fflush(stdout);
111 exit(0);
114 static void emithdrs(const char *ct, int exp, time_t lm, const char *etag,
115 bignum tl, int isr, bignum r1, bignum r2)
117 struct tm gt;
118 char dtstr[32];
120 if (isr)
121 if (isr > 0)
122 printf("Status: %u %s\r\n", 206, "Partial Content");
123 else
124 printf("Status: %u %s\r\n", 416, "Requested Range Not Satisfiable");
125 else
126 printf("Status: %u %s\r\n", 200, "OK");
127 if (exp > 0) {
128 time_t epsecs = time(NULL);
129 long esecs = 86400 * exp;
130 gt = *gmtime(&epsecs);
131 strftime(dtstr, sizeof(dtstr), "%a, %d %b %Y %H:%M:%S GMT", &gt);
132 printf("Date: %s\r\n", dtstr);
133 epsecs += esecs;
134 gt = *gmtime(&epsecs);
135 strftime(dtstr, sizeof(dtstr), "%a, %d %b %Y %H:%M:%S GMT", &gt);
136 printf("Expires: %s\r\n", dtstr);
137 printf("Cache-Control: public,max-age=%ld\r\n", esecs);
138 } else if (!exp) {
139 printf("%s\r\n", "Expires: Fri, 01 Jan 1980 00:00:00 GMT");
140 printf("%s\r\n", "Pragma: no-cache");
141 printf("%s\r\n", "Cache-Control: no-cache,max-age=0,must-revalidate");
143 printf("%s\r\n", "Accept-Ranges: bytes");
144 gt = *gmtime(&lm);
145 strftime(dtstr, sizeof(dtstr), "%a, %d %b %Y %H:%M:%S GMT", &gt);
146 printf("Last-Modified: %s\r\n", dtstr);
147 if (etag)
148 printf("ETag: %s\r\n", etag);
149 if (!isr) {
150 printf("Content-Length: %llu\r\n", tl);
151 } else if (isr > 0) {
152 printf("Content-Length: %llu\r\n", r2 - r1 + 1);
153 printf("Content-Range: bytes %llu-%llu/%llu\r\n", r1, r2, tl);
154 } else {
155 printf("Content-Range: bytes */%llu\r\n", tl);
157 if (isr >= 0) {
158 if (!ct || !*ct)
159 ct = "application/octet-stream";
160 printf("Content-Type: %s\r\n", ct);
161 printf("%s\r\n", "Vary: Accept-Encoding");
162 } else {
163 printf("%s\r\n%s\n", "Content-Type: text/plain",
164 "Requested Range Not Satisfiable");
166 printf("%s\r\n", "");
169 static void error416(time_t lm, const char *etag, bignum tl)
171 emithdrs(NULL, -1, lm, etag, tl, -1, 0, 0);
172 fflush(stdout);
173 exit(0);
176 static void die(const char *msg)
178 fprintf(stderr, "%s\n", msg);
179 fflush(stderr);
180 exit(2);
183 static void readx(int fd, void *buf, size_t count)
185 char *buff = (char *)buf;
186 int err = 0;
187 while (count && (!err || err == EINTR || err == EAGAIN || err == EWOULDBLOCK)) {
188 ssize_t amt = read(fd, buff, count);
189 if (amt == -1) {
190 err = errno;
191 continue;
193 err = 0;
194 if (!amt)
195 break;
196 buff += (size_t)amt;
197 count -= (size_t)amt;
199 if (count) {
200 if (err)
201 die("failed reading file (error)");
202 else
203 die("failed reading file (EOF)");
207 static void writex(int fd, const void *buf, size_t count)
209 const char *buff = (const char *)buf;
210 int err = 0;
211 while (count && (!err || err == EINTR || err == EAGAIN || err == EWOULDBLOCK)) {
212 ssize_t amt = write(fd, buff, count);
213 if (amt == -1) {
214 err = errno;
215 continue;
217 err = 0;
218 if (!amt)
219 break;
220 buff += (size_t)amt;
221 count -= (size_t)amt;
223 if (count) {
224 if (err)
225 die("failed writing file (error)");
226 else
227 die("failed writing file (EOF)");
231 #define SIZEPWR 15
232 static char dumpbuff[1U << SIZEPWR];
233 void dumpfile(int fd, bignum start, bignum len)
235 off_t loc = lseek(fd, (off_t)start, SEEK_SET);
236 size_t maxread;
237 if (loc != (off_t)start)
238 die("lseek failed");
239 if (start & ((bignum)(sizeof(dumpbuff) - 1)))
240 maxread = sizeof(dumpbuff) - (size_t)(start & ((bignum)(sizeof(dumpbuff) - 1)));
241 else
242 maxread = sizeof(dumpbuff);
243 while (len) {
244 size_t cnt = len > (bignum)maxread ? maxread : (size_t)len;
245 readx(fd, dumpbuff, cnt);
246 writex(STDOUT_FILENO, dumpbuff, cnt);
247 len -= (bignum)cnt;
248 maxread = sizeof(dumpbuff);
251 #undef SIZEPWR
253 int main(int argc, char *argv[])
255 int isetag = argc == 4 && !strcmp(argv[1], "--etag");
256 void (*errorexit)(unsigned,const char *,const char *) =
257 isetag ? errorfail_ : errorexit_;
258 statrec f1, f2;
259 int e1, e2, i=1;
260 bignum l1, l2, tl;
261 bignum r1=0, r2=0;
262 bignum start, length;
263 time_t lm;
264 const char *rm = !isetag ? getenv("REQUEST_METHOD") : NULL;
265 const char *hr = !isetag ? getenv("HTTP_RANGE") : NULL;
266 const char *hir = hr ? getenv("HTTP_IF_RANGE") : NULL;
267 const char *ct = NULL;
268 int expdays = -1;
269 /* "inode_inode-size-time_t_micros" each in hex up to 8 bytes gives */
270 /* "16bytes_16bytes-16bytes-16bytes" plus NUL = 70 bytes (including "") */
271 char etag[70];
272 int fd1 = -1, fd2 = -1;
274 if (isetag) {
275 i = 2;
276 } else {
277 int ch;
278 opterr = 0;
279 while ((ch = getopt(argc, argv, "c:e:")) != -1) {
280 switch(ch) {
281 case 'c':
282 ct = optarg;
283 break;
284 case 'e':
286 int v, n;
287 if (sscanf(optarg, "%i%n", &v, &n) != 1 || n != (int)strlen(optarg))
288 exit(2);
289 expdays = v;
290 break;
292 default:
293 exit(2);
296 if (argc - optind != 2)
297 exit(2);
298 if (!rm || !*rm)
299 exit(1);
300 i = optind;
303 if (!isetag && strcmp(rm, "GET") && strcmp(rm, "HEAD"))
304 errorexit(405, "Method Not Allowed", "Allow: GET,HEAD");
306 fd1 = open(argv[i], O_RDONLY);
307 e1 = fd1 >= 0 ? 0 : errno;
308 fd2 = open(argv[i+1], O_RDONLY);
309 e2 = fd2 >= 0 ? 0 : errno;
310 if (e1 == EACCES || e2 == EACCES)
311 errorexit(403, "Forbidden", NULL);
312 if (e1 == ENOENT || e1 == ENOTDIR || e2 == ENOENT || e2 == ENOTDIR)
313 errorexit(404, "Not Found", NULL);
314 e1 = fstatfunc(fd1, &f1) ? errno : 0;
315 e2 = fstatfunc(fd2, &f2) ? errno : 0;
316 if (e1 || e2)
317 errorexit(500, "Internal Server Error", NULL);
318 if (!S_ISREG(f1.st_mode) || !S_ISREG(f2.st_mode))
319 errorexit(500, "Internal Server Error", NULL);
320 if (f1.st_mtime >= f2.st_mtime)
321 lm = f1.st_mtime;
322 else
323 lm = f2.st_mtime;
324 l1 = f1.st_size;
325 l2 = f2.st_size;
326 tl = l1 + l2;
327 sprintf(etag, "\"%llx_%llx-%llx-%llx\"", (unsigned long long)f1.st_ino,
328 (unsigned long long)f2.st_ino, tl, (unsigned long long)lm * 1000000U);
330 if (isetag) {
331 close(fd2);
332 close(fd1);
333 printf("%s\n", etag);
334 exit(0);
337 if (hir && *hir && strcmp(etag, hir))
338 hr = NULL;
340 if (hr && !tl)
341 error416(lm, etag, tl); /* Range: not allowed on zero length content */
343 if (hr) {
344 /* Only one range may be specified and it must be bytes */
345 /* with a 2^64 value we could have "Range: bytes = 20-digit - 20-digit" */
346 int pos = -1;
347 int s = sscanf(hr, " %*[Bb]%*[Yy]%*[Tt]%*[Ee]%*[Ss] = %n", &pos);
348 if (s != 0 || pos < 6 || strchr(hr, ','))
349 errorexit(400, "Bad Request", NULL);
350 hr += pos;
351 if (*hr == '-') {
352 bignum trail;
353 ++hr;
354 /* It's a request for the trailing part */
355 if (strchr(hr, '-'))
356 errorexit(400, "Bad Request", NULL);
357 pos = -1;
358 s = sscanf(hr, " %llu%n", &trail, &pos);
359 if (s != 1 || pos < 1 || hr[pos])
360 errorexit(400, "Bad Request", NULL);
361 if (!trail || trail > tl)
362 error416(lm, etag, tl);
363 r1 = tl - trail;
364 r2 = tl - 1;
365 } else {
366 pos = -1;
367 s = sscanf(hr, "%llu - %n", &r1, &pos);
368 if (s != 1 || pos < 2)
369 errorexit(400, "Bad Request", NULL);
370 hr += pos;
371 if (*hr) {
372 if (*hr == '-')
373 errorexit(400, "Bad Request", NULL);
374 pos = -1;
375 s = sscanf(hr, "%llu %n", &r2, &pos);
376 if (s != 1 || pos < 1 || hr[pos])
377 errorexit(400, "Bad Request", NULL);
378 } else {
379 r2 = tl - 1;
381 if (r1 > r2 || r2 >= tl)
382 error416(lm, etag, tl);
384 start = r1;
385 length = r2 - r1 + 1;
386 } else {
387 start = 0;
388 length = tl;
391 emithdrs(ct, expdays, lm, etag, tl, hr?1:0, r1, r2);
392 fflush(stdout);
394 if (strcmp(rm, "HEAD")) {
395 if (start < l1) {
396 bignum dl = l1 - start;
397 if (dl > length) dl = length;
398 dumpfile(fd1, start, dl);
399 start += dl;
400 length -= dl;
402 if (length && start >= l1) {
403 bignum dl;
404 start -= l1;
405 dl = l2 - start;
406 if (dl > length) dl = length;
407 dumpfile(fd2, start, dl);
411 close(fd2);
412 close(fd1);
413 return 0;