3 rangecgi.c -- rangecgi utility to serve multiple files as one with range support
4 Copyright (C) 2014,2015,2016,2019,2020 Kyle J. McKay
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.
31 rangecgi ([--etag] | [-c <content-type>] [-f <filename>] [-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.
42 If -f <filename> is given, then a "Content-Disposition: attachment;
43 filename=<filename>" header will be emitted. If no -f <filename> option is
44 given, then no "Content-Disposition:" header will be emitted!
46 The default is "-m 0" which means use the latest mtime of the given files
47 when computing the ETag value. With "-m 1" always use the mtime from the
48 first file and with "-m 2" always use the mtime from the second file.
50 Other CGI parameters MUST be passed as environment variables in particular
51 REQUEST_METHOD MUST be set and to request a range, HTTP_RANGE MUST be set.
52 HTTP_IF_RANGE MAY be set. No other environment variables are examined.
54 Exit code 0 for CGI success (Status: header etc. output)
55 Exit code 1 for no REQUEST_METHOD.
56 Exit code 2 for file1 and/or file2 not given or wrong type or bad option.
57 Exit code 3 for --etag mode and file1 and/or file2 could not be opened.
59 If file1 and/or file2 is not found a 404 status will be output.
61 Normally a front end script will begin processing the initial CGI request
62 from the web server and then pass it on to this utility as appropriate.
64 If a "Range:" header is present and a non-empty "If-Range:" header is also
65 present then if the value in the "If-Range:" header does not exactly match
66 the computed "ETag:" value then the "Range:" header will be silently ignored.
69 #undef _FILE_OFFSET_BITS
70 #define _FILE_OFFSET_BITS 64
71 #include <sys/types.h>
82 #include <AvailabilityMacros.h>
83 #ifndef MAC_OS_X_VERSION_10_5
84 #define MAC_OS_X_VERSION_10_5 1050
86 #if MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_5
87 typedef struct stat statrec
;
88 #define fstatfunc(p,b) fstat(p,b)
90 typedef struct stat64 statrec
;
91 #define fstatfunc(p,b) fstat64(p,b)
94 typedef struct stat statrec
;
95 #define fstatfunc(p,b) fstat(p,b)
97 typedef unsigned long long bignum
;
99 static void errorfail_(unsigned code
, const char *status
, const char *extrahdr
)
107 static void errorexit_(unsigned code
, const char *status
, const char *extrahdr
)
109 printf("Status: %u %s\r\n", code
, status
);
110 printf("%s\r\n", "Expires: Fri, 01 Jan 1980 00:00:00 GMT");
111 printf("%s\r\n", "Pragma: no-cache");
112 printf("%s\r\n", "Cache-Control: no-cache,max-age=0,must-revalidate");
113 printf("%s\r\n", "Accept-Ranges: bytes");
115 printf("%s\r\n", extrahdr
);
116 printf("%s\r\n", "Content-Type: text/plain; charset=utf-8; format=fixed");
117 printf("%s\r\n", "");
118 printf("%s\n", status
);
123 static void emithdrs(const char *ct
, const char *fn
, int exp
, time_t lm
,
124 const char *etag
, bignum tl
, int isr
, bignum r1
, bignum r2
)
128 const char *xtra
= "";
132 printf("Status: %u %s\r\n", 206, "Partial Content");
134 printf("Status: %u %s\r\n", 416, "Requested Range Not Satisfiable");
136 printf("Status: %u %s\r\n", 200, "OK");
138 time_t epsecs
= time(NULL
);
139 long esecs
= 86400 * exp
;
140 gt
= *gmtime(&epsecs
);
141 strftime(dtstr
, sizeof(dtstr
), "%a, %d %b %Y %H:%M:%S GMT", >
);
142 printf("Date: %s\r\n", dtstr
);
144 gt
= *gmtime(&epsecs
);
145 strftime(dtstr
, sizeof(dtstr
), "%a, %d %b %Y %H:%M:%S GMT", >
);
146 printf("Expires: %s\r\n", dtstr
);
147 printf("Cache-Control: public,max-age=%ld\r\n", esecs
);
149 printf("%s\r\n", "Expires: Fri, 01 Jan 1980 00:00:00 GMT");
150 printf("%s\r\n", "Pragma: no-cache");
151 printf("%s\r\n", "Cache-Control: no-cache,max-age=0,must-revalidate");
153 printf("%s\r\n", "Accept-Ranges: bytes");
155 strftime(dtstr
, sizeof(dtstr
), "%a, %d %b %Y %H:%M:%S GMT", >
);
156 printf("Last-Modified: %s\r\n", dtstr
);
158 printf("ETag: %s\r\n", etag
);
160 printf("Content-Length: %llu\r\n", tl
);
161 } else if (isr
> 0) {
162 printf("Content-Length: %llu\r\n", r2
- r1
+ 1);
163 printf("Content-Range: bytes %llu-%llu/%llu\r\n", r1
, r2
, tl
);
165 printf("Content-Range: bytes */%llu\r\n", tl
);
169 ct
= "application/octet-stream";
170 printf("Content-Type: %s\r\n", ct
);
172 printf("Content-Disposition: attachment; filename=\"%s\"\r\n", fn
);
173 printf("%s\r\n", "Vary: Accept-Encoding");
175 printf("%s\r\n", "Content-Type: text/plain; charset=utf-8; format=fixed");
176 xtra
= "Requested Range Not Satisfiable\n";
178 printf("\r\n%s", xtra
);
181 static void error416(time_t lm
, const char *etag
, bignum tl
)
183 emithdrs(NULL
, NULL
, -1, lm
, etag
, tl
, -1, 0, 0);
188 static void die(const char *msg
)
190 fprintf(stderr
, "%s\n", msg
);
195 static void readx(int fd
, void *buf
, size_t count
)
197 char *buff
= (char *)buf
;
199 while (count
&& (!err
|| err
== EINTR
|| err
== EAGAIN
|| err
== EWOULDBLOCK
)) {
200 ssize_t amt
= read(fd
, buff
, count
);
209 count
-= (size_t)amt
;
213 die("failed reading file (error)");
215 die("failed reading file (EOF)");
219 static void writex(int fd
, const void *buf
, size_t count
)
221 const char *buff
= (const char *)buf
;
223 while (count
&& (!err
|| err
== EINTR
|| err
== EAGAIN
|| err
== EWOULDBLOCK
)) {
224 ssize_t amt
= write(fd
, buff
, count
);
233 count
-= (size_t)amt
;
237 die("failed writing file (error)");
239 die("failed writing file (EOF)");
244 static char dumpbuff
[1U << SIZEPWR
];
245 void dumpfile(int fd
, bignum start
, bignum len
)
247 off_t loc
= lseek(fd
, (off_t
)start
, SEEK_SET
);
249 if (loc
!= (off_t
)start
)
251 if (start
& ((bignum
)(sizeof(dumpbuff
) - 1)))
252 maxread
= sizeof(dumpbuff
) - (size_t)(start
& ((bignum
)(sizeof(dumpbuff
) - 1)));
254 maxread
= sizeof(dumpbuff
);
256 size_t cnt
= len
> (bignum
)maxread
? maxread
: (size_t)len
;
257 readx(fd
, dumpbuff
, cnt
);
258 writex(STDOUT_FILENO
, dumpbuff
, cnt
);
260 maxread
= sizeof(dumpbuff
);
265 int main(int argc
, char *argv
[])
268 void (*errorexit
)(unsigned,const char *,const char *);
273 bignum start
, length
;
275 const char *rm
= NULL
;
276 const char *hr
= NULL
;
277 const char *hir
= NULL
;
278 const char *ct
= NULL
;
279 const char *fn
= NULL
;
280 int expdays
= -1, opt_e
= 0;
281 /* "inode_inode-size-time_t_micros" each in hex up to 8 bytes gives */
282 /* "16bytes_16bytes-16bytes-16bytes" plus NUL = 70 bytes (including "") */
284 int fd1
= -1, fd2
= -1;
290 if (optind
< argc
&& !strcmp(argv
[optind
], "--etag")) {
294 ch
= getopt(argc
, argv
, "c:e:f:m:");
311 if (sscanf(optarg
, "%i%n", &v
, &n
) != 1 || n
!= (int)strlen(optarg
))
318 if (!optarg
[0] || optarg
[1])
320 if (optarg
[0] != '0' && optarg
[0] != '1' && optarg
[0] != '2')
322 mno
= optarg
[0] - '0';
328 if (argc
- optind
!= 2)
333 if (ct
|| fn
|| opt_e
)
335 errorexit
= errorfail_
;
337 rm
= getenv("REQUEST_METHOD");
340 hr
= getenv("HTTP_RANGE");
342 hir
= getenv("HTTP_IF_RANGE");
343 errorexit
= errorexit_
;
344 if (strcmp(rm
, "GET") && strcmp(rm
, "HEAD"))
345 errorexit(405, "Method Not Allowed", "Allow: GET,HEAD");
348 fd1
= open(argv
[i
], O_RDONLY
);
349 e1
= fd1
>= 0 ? 0 : errno
;
350 fd2
= open(argv
[i
+1], O_RDONLY
);
351 e2
= fd2
>= 0 ? 0 : errno
;
352 if (e1
== EACCES
|| e2
== EACCES
)
353 errorexit(403, "Forbidden", NULL
);
354 if (e1
== ENOENT
|| e1
== ENOTDIR
|| e2
== ENOENT
|| e2
== ENOTDIR
)
355 errorexit(404, "Not Found", NULL
);
356 e1
= fstatfunc(fd1
, &f1
) ? errno
: 0;
357 e2
= fstatfunc(fd2
, &f2
) ? errno
: 0;
359 errorexit(500, "Internal Server Error", NULL
);
360 if (!S_ISREG(f1
.st_mode
) || !S_ISREG(f2
.st_mode
))
361 errorexit(500, "Internal Server Error", NULL
);
366 else if (f1
.st_mtime
>= f2
.st_mtime
)
373 sprintf(etag
, "\"%llx_%llx-%llx-%llx\"", (unsigned long long)f1
.st_ino
,
374 (unsigned long long)f2
.st_ino
, tl
, (unsigned long long)lm
* 1000000U);
379 printf("%s\n", etag
);
383 if (hir
&& *hir
&& strcmp(etag
, hir
))
387 error416(lm
, etag
, tl
); /* Range: not allowed on zero length content */
390 /* Only one range may be specified and it must be bytes */
391 /* with a 2^64 value we could have "Range: bytes = 20-digit - 20-digit" */
393 int s
= sscanf(hr
, " %*[Bb]%*[Yy]%*[Tt]%*[Ee]%*[Ss] = %n", &pos
);
394 if (s
!= 0 || pos
< 6 || strchr(hr
, ','))
395 errorexit(400, "Bad Request", NULL
);
400 /* It's a request for the trailing part */
402 errorexit(400, "Bad Request", NULL
);
404 s
= sscanf(hr
, " %llu%n", &trail
, &pos
);
405 if (s
!= 1 || pos
< 1 || hr
[pos
])
406 errorexit(400, "Bad Request", NULL
);
407 if (!trail
|| trail
> tl
)
408 error416(lm
, etag
, tl
);
413 s
= sscanf(hr
, "%llu - %n", &r1
, &pos
);
414 if (s
!= 1 || pos
< 2)
415 errorexit(400, "Bad Request", NULL
);
419 errorexit(400, "Bad Request", NULL
);
421 s
= sscanf(hr
, "%llu %n", &r2
, &pos
);
422 if (s
!= 1 || pos
< 1 || hr
[pos
])
423 errorexit(400, "Bad Request", NULL
);
427 if (r1
> r2
|| r2
>= tl
)
428 error416(lm
, etag
, tl
);
431 length
= r2
- r1
+ 1;
437 emithdrs(ct
, fn
, expdays
, lm
, etag
, tl
, hr
?1:0, r1
, r2
);
440 if (strcmp(rm
, "HEAD")) {
442 bignum dl
= l1
- start
;
443 if (dl
> length
) dl
= length
;
444 dumpfile(fd1
, start
, dl
);
448 if (length
&& start
>= l1
) {
452 if (dl
> length
) dl
= length
;
453 dumpfile(fd2
, start
, dl
);