install: add bundle utility
[girocco.git] / src / rangecgi.c
blob54bca6870f9848e24977896f8fb81301b5b477d5
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] 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 Other CGI parameters MUST be passed as environment variables in particular
37 REQUEST_METHOD MUST be set and to request a range, HTTP_RANGE MUST be set.
38 HTTP_IF_RANGE MAY be set. No other environment variables are examined.
40 Exit code 0 for CGI success (Status: header etc. output)
41 Exit code 1 for no REQUEST_METHOD.
42 Exit code 2 for file1 and/or file2 not given or wrong type or bad option.
43 Exit code 3 for --etag mode and file1 and/or file2 could not be opened.
45 If file1 and/or file2 is not found a 404 status will be output.
47 Normally a front end script will begin processing the initial CGI request
48 from the web server and then pass it on to this utility as appropriate.
50 If a "Range:" header is present and a non-empty "If-Range:" header is also
51 present then if the value in the "If-Range:" header does not exactly match
52 the computed "ETag:" value then the "Range:" header will be silently ignored.
55 #undef _FILE_OFFSET_BITS
56 #define _FILE_OFFSET_BITS 64
57 #include <sys/types.h>
58 #include <sys/uio.h>
59 #include <errno.h>
60 #include <fcntl.h>
61 #include <stdio.h>
62 #include <stdlib.h>
63 #include <string.h>
64 #include <sys/stat.h>
65 #include <time.h>
66 #include <unistd.h>
67 #ifdef __APPLE__
68 #include <AvailabilityMacros.h>
69 #ifndef MAC_OS_X_VERSION_10_5
70 #define MAC_OS_X_VERSION_10_5 1050
71 #endif
72 #if MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_5
73 typedef struct stat statrec;
74 #define fstatfunc(p,b) fstat(p,b)
75 #else
76 typedef struct stat64 statrec;
77 #define fstatfunc(p,b) fstat64(p,b)
78 #endif
79 #else
80 typedef struct stat statrec;
81 #define fstatfunc(p,b) fstat(p,b)
82 #endif
83 typedef unsigned long long bignum;
85 static void errorfail_(unsigned code, const char *status, const char *extrahdr)
87 (void)code;
88 (void)status;
89 (void)extrahdr;
90 exit(3);
93 static void errorexit_(unsigned code, const char *status, const char *extrahdr)
95 printf("Status: %u %s\r\n", code, status);
96 printf("%s\r\n", "Expires: Fri, 01 Jan 1980 00:00:00 GMT");
97 printf("%s\r\n", "Pragma: no-cache");
98 printf("%s\r\n", "Cache-Control: no-cache, max-age=0, must-revalidate");
99 printf("%s\r\n", "Accept-Ranges: bytes");
100 if (extrahdr)
101 printf("%s\r\n", extrahdr);
102 printf("%s\r\n", "Content-Type: text/plain");
103 printf("%s\r\n", "");
104 printf("%s\n", status);
105 fflush(stdout);
106 exit(0);
109 static void emithdrs(time_t lm, const char *etag, bignum tl, int isr, bignum r1, bignum r2)
111 struct tm gt;
112 char lmstr[32];
114 gt = *gmtime(&lm);
115 strftime(lmstr, sizeof(lmstr), "%a, %d %b %Y %H:%M:%S GMT", &gt);
116 if (isr)
117 if (isr > 0)
118 printf("Status: %u %s\r\n", 206, "Partial Content");
119 else
120 printf("Status: %u %s\r\n", 416, "Requested Range Not Satisfiable");
121 else
122 printf("Status: %u %s\r\n", 200, "OK");
123 printf("%s\r\n", "Accept-Ranges: bytes");
124 printf("Last-Modified: %s\r\n", lmstr);
125 if (etag)
126 printf("ETag: %s\r\n", etag);
127 if (!isr) {
128 printf("Content-Length: %llu\r\n", tl);
129 } else if (isr > 0) {
130 printf("Content-Length: %llu\r\n", r2 - r1 + 1);
131 printf("Content-Range: bytes %llu-%llu/%llu\r\n", r1, r2, tl);
132 } else {
133 printf("Content-Range: bytes */%llu\r\n", tl);
135 if (isr >= 0)
136 printf("%s\r\n", "Content-Type: application/octet-stream");
137 else
138 printf("%s\r\n%s\n", "Content-Type: text/plain",
139 "Requested Range Not Satisfiable");
140 printf("%s\r\n", "");
143 static void error416(time_t lm, const char *etag, bignum tl)
145 emithdrs(lm, etag, tl, -1, 0, 0);
146 fflush(stdout);
147 exit(0);
150 static void die(const char *msg)
152 fprintf(stderr, "%s\n", msg);
153 fflush(stderr);
154 exit(2);
157 static void readx(int fd, void *buf, size_t count)
159 char *buff = (char *)buf;
160 int err = 0;
161 while (count && (!err || err == EINTR || err == EAGAIN || err == EWOULDBLOCK)) {
162 ssize_t amt = read(fd, buff, count);
163 if (amt == -1) {
164 err = errno;
165 continue;
167 err = 0;
168 if (!amt)
169 break;
170 buff += (size_t)amt;
171 count -= (size_t)amt;
173 if (count) {
174 if (err)
175 die("failed reading file (error)");
176 else
177 die("failed reading file (EOF)");
181 static void writex(int fd, const void *buf, size_t count)
183 const char *buff = (const char *)buf;
184 int err = 0;
185 while (count && (!err || err == EINTR || err == EAGAIN || err == EWOULDBLOCK)) {
186 ssize_t amt = write(fd, buff, count);
187 if (amt == -1) {
188 err = errno;
189 continue;
191 err = 0;
192 if (!amt)
193 break;
194 buff += (size_t)amt;
195 count -= (size_t)amt;
197 if (count) {
198 if (err)
199 die("failed writing file (error)");
200 else
201 die("failed writing file (EOF)");
205 #define SIZEPWR 15
206 static char dumpbuff[1U << SIZEPWR];
207 void dumpfile(int fd, bignum start, bignum len)
209 off_t loc = lseek(fd, (off_t)start, SEEK_SET);
210 size_t maxread;
211 if (loc != (off_t)start)
212 die("lseek failed");
213 if (start & ((bignum)(sizeof(dumpbuff) - 1)))
214 maxread = sizeof(dumpbuff) - (size_t)(start & ((bignum)(sizeof(dumpbuff) - 1)));
215 else
216 maxread = sizeof(dumpbuff);
217 while (len) {
218 size_t cnt = len > (bignum)maxread ? maxread : (size_t)len;
219 readx(fd, dumpbuff, cnt);
220 writex(STDOUT_FILENO, dumpbuff, cnt);
221 len -= (bignum)cnt;
222 maxread = sizeof(dumpbuff);
225 #undef SIZEPWR
227 int main(int argc, char *argv[])
229 void (*errorexit)(unsigned,const char *,const char *) =
230 argc == 3 ? errorexit_ : errorfail_;
231 statrec f1, f2;
232 int e1, e2, i=1;
233 bignum l1, l2, tl;
234 bignum r1=0, r2=0;
235 bignum start, length;
236 time_t lm;
237 const char *rm = argc == 3 ? getenv("REQUEST_METHOD") : NULL;
238 const char *hr = argc == 3 ? getenv("HTTP_RANGE") : NULL;
239 const char *hir = hr ? getenv("HTTP_IF_RANGE") : NULL;
240 /* "inode_inode-size-time_t_micros" each in hex up to 8 bytes gives */
241 /* "16bytes_16bytes-16bytes-16bytes" plus NUL = 70 bytes (including "") */
242 char etag[70];
243 int fd1 = -1, fd2 = -1;
245 if (argc == 3 && (!rm || !*rm))
246 exit(1);
248 if (argc < 3 || argc > 4)
249 exit(2);
250 if (argc == 4) {
251 if (strcmp(argv[1], "--etag"))
252 exit(2);
253 i = 2;
256 if (argc == 3 && strcmp(rm, "GET") && strcmp(rm, "HEAD"))
257 errorexit(405, "Method Not Allowed", "Allow: GET,HEAD");
259 fd1 = open(argv[i], O_RDONLY);
260 e1 = fd1 >= 0 ? 0 : errno;
261 fd2 = open(argv[i+1], O_RDONLY);
262 e2 = fd2 >= 0 ? 0 : errno;
263 if (e1 == EACCES || e2 == EACCES)
264 errorexit(403, "Forbidden", NULL);
265 if (e1 == ENOENT || e1 == ENOTDIR || e2 == ENOENT || e2 == ENOTDIR)
266 errorexit(404, "Not Found", NULL);
267 e1 = fstatfunc(fd1, &f1) ? errno : 0;
268 e2 = fstatfunc(fd2, &f2) ? errno : 0;
269 if (e1 || e2)
270 errorexit(500, "Internal Server Error", NULL);
271 if (!S_ISREG(f1.st_mode) || !S_ISREG(f2.st_mode))
272 errorexit(500, "Internal Server Error", NULL);
273 if (f1.st_mtime >= f2.st_mtime)
274 lm = f1.st_mtime;
275 else
276 lm = f2.st_mtime;
277 l1 = f1.st_size;
278 l2 = f2.st_size;
279 tl = l1 + l2;
280 sprintf(etag, "\"%llx_%llx-%llx-%llx\"", (unsigned long long)f1.st_ino,
281 (unsigned long long)f2.st_ino, tl, (unsigned long long)lm * 1000000U);
283 if (argc == 4) {
284 close(fd2);
285 close(fd1);
286 printf("%s\n", etag);
287 exit(0);
290 if (hir && *hir && strcmp(etag, hir))
291 hr = NULL;
293 if (hr && !tl)
294 error416(lm, etag, tl); /* Range: not allowed on zero length content */
296 if (hr) {
297 /* Only one range may be specified and it must be bytes */
298 /* with a 2^64 value we could have "Range: bytes = 20-digit - 20-digit" */
299 int pos = -1;
300 int s = sscanf(hr, " %*[Bb]%*[Yy]%*[Tt]%*[Ee]%*[Ss] = %n", &pos);
301 if (s != 0 || pos < 6 || strchr(hr, ','))
302 errorexit(400, "Bad Request", NULL);
303 hr += pos;
304 if (*hr == '-') {
305 bignum trail;
306 ++hr;
307 /* It's a request for the trailing part */
308 if (strchr(hr, '-'))
309 errorexit(400, "Bad Request", NULL);
310 pos = -1;
311 s = sscanf(hr, " %llu%n", &trail, &pos);
312 if (s != 1 || pos < 1 || hr[pos])
313 errorexit(400, "Bad Request", NULL);
314 if (!trail || trail > tl)
315 error416(lm, etag, tl);
316 r1 = tl - trail;
317 r2 = tl - 1;
318 } else {
319 pos = -1;
320 s = sscanf(hr, "%llu - %n", &r1, &pos);
321 if (s != 1 || pos < 2)
322 errorexit(400, "Bad Request", NULL);
323 hr += pos;
324 if (*hr) {
325 if (*hr == '-')
326 errorexit(400, "Bad Request", NULL);
327 pos = -1;
328 s = sscanf(hr, "%llu %n", &r2, &pos);
329 if (s != 1 || pos < 1 || hr[pos])
330 errorexit(400, "Bad Request", NULL);
331 } else {
332 r2 = tl - 1;
334 if (r1 > r2 || r2 >= tl)
335 error416(lm, etag, tl);
337 start = r1;
338 length = r2 - r1 + 1;
339 } else {
340 start = 0;
341 length = tl;
344 if (!strcmp(rm, "HEAD")) {
345 emithdrs(lm, etag, tl, hr?1:0, r1, r2);
346 fflush(stdout);
347 exit(0);
350 emithdrs(lm, etag, tl, hr?1:0, r1, r2);
351 fflush(stdout);
353 if (start < l1) {
354 bignum dl = l1 - start;
355 if (dl > length) dl = length;
356 dumpfile(fd1, start, dl);
357 start += dl;
358 length -= dl;
360 if (length && start >= l1) {
361 bignum dl;
362 start -= l1;
363 dl = l2 - start;
364 if (dl > length) dl = length;
365 dumpfile(fd2, start, dl);
368 close(fd2);
369 close(fd1);
370 return 0;