3 rangecgi.c -- rangecgi utility to serve multiple files as one with range support
4 Copyright (C) 2014,2015,2016,2019 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>] [-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>
79 #include <AvailabilityMacros.h>
80 #ifndef MAC_OS_X_VERSION_10_5
81 #define MAC_OS_X_VERSION_10_5 1050
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)
87 typedef struct stat64 statrec
;
88 #define fstatfunc(p,b) fstat64(p,b)
91 typedef struct stat statrec
;
92 #define fstatfunc(p,b) fstat(p,b)
94 typedef unsigned long long bignum
;
96 static void errorfail_(unsigned code
, const char *status
, const char *extrahdr
)
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");
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
);
120 static void emithdrs(const char *ct
, int exp
, time_t lm
, const char *etag
,
121 bignum tl
, int isr
, bignum r1
, bignum r2
)
125 const char *xtra
= "";
129 printf("Status: %u %s\r\n", 206, "Partial Content");
131 printf("Status: %u %s\r\n", 416, "Requested Range Not Satisfiable");
133 printf("Status: %u %s\r\n", 200, "OK");
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", >
);
139 printf("Date: %s\r\n", dtstr
);
141 gt
= *gmtime(&epsecs
);
142 strftime(dtstr
, sizeof(dtstr
), "%a, %d %b %Y %H:%M:%S GMT", >
);
143 printf("Expires: %s\r\n", dtstr
);
144 printf("Cache-Control: public,max-age=%ld\r\n", esecs
);
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");
152 strftime(dtstr
, sizeof(dtstr
), "%a, %d %b %Y %H:%M:%S GMT", >
);
153 printf("Last-Modified: %s\r\n", dtstr
);
155 printf("ETag: %s\r\n", etag
);
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
);
162 printf("Content-Range: bytes */%llu\r\n", tl
);
166 ct
= "application/octet-stream";
167 printf("Content-Type: %s\r\n", ct
);
168 printf("%s\r\n", "Vary: Accept-Encoding");
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);
183 static void die(const char *msg
)
185 fprintf(stderr
, "%s\n", msg
);
190 static void readx(int fd
, void *buf
, size_t count
)
192 char *buff
= (char *)buf
;
194 while (count
&& (!err
|| err
== EINTR
|| err
== EAGAIN
|| err
== EWOULDBLOCK
)) {
195 ssize_t amt
= read(fd
, buff
, count
);
204 count
-= (size_t)amt
;
208 die("failed reading file (error)");
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
;
218 while (count
&& (!err
|| err
== EINTR
|| err
== EAGAIN
|| err
== EWOULDBLOCK
)) {
219 ssize_t amt
= write(fd
, buff
, count
);
228 count
-= (size_t)amt
;
232 die("failed writing file (error)");
234 die("failed writing file (EOF)");
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
);
244 if (loc
!= (off_t
)start
)
246 if (start
& ((bignum
)(sizeof(dumpbuff
) - 1)))
247 maxread
= sizeof(dumpbuff
) - (size_t)(start
& ((bignum
)(sizeof(dumpbuff
) - 1)));
249 maxread
= sizeof(dumpbuff
);
251 size_t cnt
= len
> (bignum
)maxread
? maxread
: (size_t)len
;
252 readx(fd
, dumpbuff
, cnt
);
253 writex(STDOUT_FILENO
, dumpbuff
, cnt
);
255 maxread
= sizeof(dumpbuff
);
260 int main(int argc
, char *argv
[])
263 void (*errorexit
)(unsigned,const char *,const char *);
268 bignum start
, length
;
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 "") */
278 int fd1
= -1, fd2
= -1;
284 if (optind
< argc
&& !strcmp(argv
[optind
], "--etag")) {
288 ch
= getopt(argc
, argv
, "c:e:m:");
302 if (sscanf(optarg
, "%i%n", &v
, &n
) != 1 || n
!= (int)strlen(optarg
))
309 if (!optarg
[0] || optarg
[1])
311 if (optarg
[0] != '0' && optarg
[0] != '1' && optarg
[0] != '2')
313 mno
= optarg
[0] - '0';
319 if (argc
- optind
!= 2)
326 errorexit
= errorfail_
;
328 rm
= getenv("REQUEST_METHOD");
331 hr
= getenv("HTTP_RANGE");
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;
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
);
357 else if (f1
.st_mtime
>= f2
.st_mtime
)
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);
370 printf("%s\n", etag
);
374 if (hir
&& *hir
&& strcmp(etag
, hir
))
378 error416(lm
, etag
, tl
); /* Range: not allowed on zero length content */
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" */
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
);
391 /* It's a request for the trailing part */
393 errorexit(400, "Bad Request", NULL
);
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
);
404 s
= sscanf(hr
, "%llu - %n", &r1
, &pos
);
405 if (s
!= 1 || pos
< 2)
406 errorexit(400, "Bad Request", NULL
);
410 errorexit(400, "Bad Request", NULL
);
412 s
= sscanf(hr
, "%llu %n", &r2
, &pos
);
413 if (s
!= 1 || pos
< 1 || hr
[pos
])
414 errorexit(400, "Bad Request", NULL
);
418 if (r1
> r2
|| r2
>= tl
)
419 error416(lm
, etag
, tl
);
422 length
= r2
- r1
+ 1;
428 emithdrs(ct
, expdays
, lm
, etag
, tl
, hr
?1:0, r1
, r2
);
431 if (strcmp(rm
, "HEAD")) {
433 bignum dl
= l1
- start
;
434 if (dl
> length
) dl
= length
;
435 dumpfile(fd1
, start
, dl
);
439 if (length
&& start
>= l1
) {
443 if (dl
> length
) dl
= length
;
444 dumpfile(fd2
, start
, dl
);