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.
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>
78 #include <AvailabilityMacros.h>
79 #ifndef MAC_OS_X_VERSION_10_5
80 #define MAC_OS_X_VERSION_10_5 1050
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)
86 typedef struct stat64 statrec
;
87 #define fstatfunc(p,b) fstat64(p,b)
90 typedef struct stat statrec
;
91 #define fstatfunc(p,b) fstat(p,b)
93 typedef unsigned long long bignum
;
95 static void errorfail_(unsigned code
, const char *status
, const char *extrahdr
)
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");
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
);
119 static void emithdrs(const char *ct
, int exp
, time_t lm
, const char *etag
,
120 bignum tl
, int isr
, bignum r1
, bignum r2
)
127 printf("Status: %u %s\r\n", 206, "Partial Content");
129 printf("Status: %u %s\r\n", 416, "Requested Range Not Satisfiable");
131 printf("Status: %u %s\r\n", 200, "OK");
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", >
);
137 printf("Date: %s\r\n", dtstr
);
139 gt
= *gmtime(&epsecs
);
140 strftime(dtstr
, sizeof(dtstr
), "%a, %d %b %Y %H:%M:%S GMT", >
);
141 printf("Expires: %s\r\n", dtstr
);
142 printf("Cache-Control: public,max-age=%ld\r\n", esecs
);
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");
150 strftime(dtstr
, sizeof(dtstr
), "%a, %d %b %Y %H:%M:%S GMT", >
);
151 printf("Last-Modified: %s\r\n", dtstr
);
153 printf("ETag: %s\r\n", etag
);
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
);
160 printf("Content-Range: bytes */%llu\r\n", tl
);
164 ct
= "application/octet-stream";
165 printf("Content-Type: %s\r\n", ct
);
166 printf("%s\r\n", "Vary: Accept-Encoding");
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);
181 static void die(const char *msg
)
183 fprintf(stderr
, "%s\n", msg
);
188 static void readx(int fd
, void *buf
, size_t count
)
190 char *buff
= (char *)buf
;
192 while (count
&& (!err
|| err
== EINTR
|| err
== EAGAIN
|| err
== EWOULDBLOCK
)) {
193 ssize_t amt
= read(fd
, buff
, count
);
202 count
-= (size_t)amt
;
206 die("failed reading file (error)");
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
;
216 while (count
&& (!err
|| err
== EINTR
|| err
== EAGAIN
|| err
== EWOULDBLOCK
)) {
217 ssize_t amt
= write(fd
, buff
, count
);
226 count
-= (size_t)amt
;
230 die("failed writing file (error)");
232 die("failed writing file (EOF)");
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
);
242 if (loc
!= (off_t
)start
)
244 if (start
& ((bignum
)(sizeof(dumpbuff
) - 1)))
245 maxread
= sizeof(dumpbuff
) - (size_t)(start
& ((bignum
)(sizeof(dumpbuff
) - 1)));
247 maxread
= sizeof(dumpbuff
);
249 size_t cnt
= len
> (bignum
)maxread
? maxread
: (size_t)len
;
250 readx(fd
, dumpbuff
, cnt
);
251 writex(STDOUT_FILENO
, dumpbuff
, cnt
);
253 maxread
= sizeof(dumpbuff
);
258 int main(int argc
, char *argv
[])
261 void (*errorexit
)(unsigned,const char *,const char *);
266 bignum start
, length
;
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 "") */
276 int fd1
= -1, fd2
= -1;
282 if (optind
< argc
&& !strcmp(argv
[optind
], "--etag")) {
286 ch
= getopt(argc
, argv
, "c:e:m:");
300 if (sscanf(optarg
, "%i%n", &v
, &n
) != 1 || n
!= (int)strlen(optarg
))
307 if (!optarg
[0] || optarg
[1])
309 if (optarg
[0] != '0' && optarg
[0] != '1' && optarg
[0] != '2')
311 mno
= optarg
[0] - '0';
317 if (argc
- optind
!= 2)
324 errorexit
= errorfail_
;
326 rm
= getenv("REQUEST_METHOD");
329 hr
= getenv("HTTP_RANGE");
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;
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
);
355 else if (f1
.st_mtime
>= f2
.st_mtime
)
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);
368 printf("%s\n", etag
);
372 if (hir
&& *hir
&& strcmp(etag
, hir
))
376 error416(lm
, etag
, tl
); /* Range: not allowed on zero length content */
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" */
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
);
389 /* It's a request for the trailing part */
391 errorexit(400, "Bad Request", NULL
);
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
);
402 s
= sscanf(hr
, "%llu - %n", &r1
, &pos
);
403 if (s
!= 1 || pos
< 2)
404 errorexit(400, "Bad Request", NULL
);
408 errorexit(400, "Bad Request", NULL
);
410 s
= sscanf(hr
, "%llu %n", &r2
, &pos
);
411 if (s
!= 1 || pos
< 1 || hr
[pos
])
412 errorexit(400, "Bad Request", NULL
);
416 if (r1
> r2
|| r2
>= tl
)
417 error416(lm
, etag
, tl
);
420 length
= r2
- r1
+ 1;
426 emithdrs(ct
, expdays
, lm
, etag
, tl
, hr
?1:0, r1
, r2
);
429 if (strcmp(rm
, "HEAD")) {
431 bignum dl
= l1
- start
;
432 if (dl
> length
) dl
= length
;
433 dumpfile(fd1
, start
, dl
);
437 if (length
&& start
>= l1
) {
441 if (dl
> length
) dl
= length
;
442 dumpfile(fd2
, start
, dl
);