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
72 #include <AvailabilityMacros.h>
73 #ifndef MAC_OS_X_VERSION_10_5
74 #define MAC_OS_X_VERSION_10_5 1050
76 #if MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5
77 #include <Availability.h>
79 #undef __OSX_AVAILABLE_BUT_DEPRECATED
80 #define __OSX_AVAILABLE_BUT_DEPRECATED(x,y,z,w)
81 #endif /* __APPLE__ */
82 #include <sys/types.h>
93 #if MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_5
94 typedef struct stat statrec
;
95 #define fstatfunc(p,b) fstat(p,b)
97 typedef struct stat64 statrec
;
98 #define fstatfunc(p,b) fstat64(p,b)
100 #else /* !__APPLE__ */
101 typedef struct stat statrec
;
102 #define fstatfunc(p,b) fstat(p,b)
103 #endif /* !__APPLE__ */
104 typedef unsigned long long bignum
;
106 static void errorfail_(unsigned code
, const char *status
, const char *extrahdr
)
114 static void errorexit_(unsigned code
, const char *status
, const char *extrahdr
)
116 printf("Status: %u %s\r\n", code
, status
);
117 printf("%s\r\n", "Expires: Fri, 01 Jan 1980 00:00:00 GMT");
118 printf("%s\r\n", "Pragma: no-cache");
119 printf("%s\r\n", "Cache-Control: no-cache,max-age=0,must-revalidate");
120 printf("%s\r\n", "Accept-Ranges: bytes");
122 printf("%s\r\n", extrahdr
);
123 printf("%s\r\n", "Content-Type: text/plain; charset=utf-8; format=fixed");
124 printf("%s\r\n", "");
125 printf("%s\n", status
);
130 static void emithdrs(const char *ct
, const char *fn
, int exp
, time_t lm
,
131 const char *etag
, bignum tl
, int isr
, bignum r1
, bignum r2
)
135 const char *xtra
= "";
139 printf("Status: %u %s\r\n", 206, "Partial Content");
141 printf("Status: %u %s\r\n", 416, "Requested Range Not Satisfiable");
143 printf("Status: %u %s\r\n", 200, "OK");
145 time_t epsecs
= time(NULL
);
146 long esecs
= 86400 * exp
;
147 gt
= *gmtime(&epsecs
);
148 strftime(dtstr
, sizeof(dtstr
), "%a, %d %b %Y %H:%M:%S GMT", >
);
149 printf("Date: %s\r\n", dtstr
);
151 gt
= *gmtime(&epsecs
);
152 strftime(dtstr
, sizeof(dtstr
), "%a, %d %b %Y %H:%M:%S GMT", >
);
153 printf("Expires: %s\r\n", dtstr
);
154 printf("Cache-Control: public,max-age=%ld\r\n", esecs
);
156 printf("%s\r\n", "Expires: Fri, 01 Jan 1980 00:00:00 GMT");
157 printf("%s\r\n", "Pragma: no-cache");
158 printf("%s\r\n", "Cache-Control: no-cache,max-age=0,must-revalidate");
160 printf("%s\r\n", "Accept-Ranges: bytes");
162 strftime(dtstr
, sizeof(dtstr
), "%a, %d %b %Y %H:%M:%S GMT", >
);
163 printf("Last-Modified: %s\r\n", dtstr
);
165 printf("ETag: %s\r\n", etag
);
167 printf("Content-Length: %llu\r\n", tl
);
168 } else if (isr
> 0) {
169 printf("Content-Length: %llu\r\n", r2
- r1
+ 1);
170 printf("Content-Range: bytes %llu-%llu/%llu\r\n", r1
, r2
, tl
);
172 printf("Content-Range: bytes */%llu\r\n", tl
);
176 ct
= "application/octet-stream";
177 printf("Content-Type: %s\r\n", ct
);
179 printf("Content-Disposition: attachment; filename=\"%s\"\r\n", fn
);
180 printf("%s\r\n", "Vary: Accept-Encoding");
182 printf("%s\r\n", "Content-Type: text/plain; charset=utf-8; format=fixed");
183 xtra
= "Requested Range Not Satisfiable\n";
185 printf("\r\n%s", xtra
);
188 static void error416(time_t lm
, const char *etag
, bignum tl
)
190 emithdrs(NULL
, NULL
, -1, lm
, etag
, tl
, -1, 0, 0);
195 static void die(const char *msg
)
197 fprintf(stderr
, "%s\n", msg
);
202 static void readx(int fd
, void *buf
, size_t count
)
204 char *buff
= (char *)buf
;
206 while (count
&& (!err
|| err
== EINTR
|| err
== EAGAIN
|| err
== EWOULDBLOCK
)) {
207 ssize_t amt
= read(fd
, buff
, count
);
216 count
-= (size_t)amt
;
220 die("failed reading file (error)");
222 die("failed reading file (EOF)");
226 static void writex(int fd
, const void *buf
, size_t count
)
228 const char *buff
= (const char *)buf
;
230 while (count
&& (!err
|| err
== EINTR
|| err
== EAGAIN
|| err
== EWOULDBLOCK
)) {
231 ssize_t amt
= write(fd
, buff
, count
);
240 count
-= (size_t)amt
;
244 die("failed writing file (error)");
246 die("failed writing file (EOF)");
251 static char dumpbuff
[1U << SIZEPWR
];
252 void dumpfile(int fd
, bignum start
, bignum len
)
254 off_t loc
= lseek(fd
, (off_t
)start
, SEEK_SET
);
256 if (loc
!= (off_t
)start
)
258 if (start
& ((bignum
)(sizeof(dumpbuff
) - 1)))
259 maxread
= sizeof(dumpbuff
) - (size_t)(start
& ((bignum
)(sizeof(dumpbuff
) - 1)));
261 maxread
= sizeof(dumpbuff
);
263 size_t cnt
= len
> (bignum
)maxread
? maxread
: (size_t)len
;
264 readx(fd
, dumpbuff
, cnt
);
265 writex(STDOUT_FILENO
, dumpbuff
, cnt
);
267 maxread
= sizeof(dumpbuff
);
272 int main(int argc
, char *argv
[])
275 void (*errorexit
)(unsigned,const char *,const char *);
280 bignum start
, length
;
282 const char *rm
= NULL
;
283 const char *hr
= NULL
;
284 const char *hir
= NULL
;
285 const char *ct
= NULL
;
286 const char *fn
= NULL
;
287 int expdays
= -1, opt_e
= 0;
288 /* "inode_inode-size-time_t_micros" each in hex up to 8 bytes gives */
289 /* "16bytes_16bytes-16bytes-16bytes" plus NUL = 70 bytes (including "") */
291 int fd1
= -1, fd2
= -1;
297 if (optind
< argc
&& !strcmp(argv
[optind
], "--etag")) {
301 ch
= getopt(argc
, argv
, "c:e:f:m:");
318 if (sscanf(optarg
, "%i%n", &v
, &n
) != 1 || n
!= (int)strlen(optarg
))
325 if (!optarg
[0] || optarg
[1])
327 if (optarg
[0] != '0' && optarg
[0] != '1' && optarg
[0] != '2')
329 mno
= optarg
[0] - '0';
335 if (argc
- optind
!= 2)
340 if (ct
|| fn
|| opt_e
)
342 errorexit
= errorfail_
;
344 rm
= getenv("REQUEST_METHOD");
347 hr
= getenv("HTTP_RANGE");
349 hir
= getenv("HTTP_IF_RANGE");
350 errorexit
= errorexit_
;
351 if (strcmp(rm
, "GET") && strcmp(rm
, "HEAD"))
352 errorexit(405, "Method Not Allowed", "Allow: GET,HEAD");
355 fd1
= open(argv
[i
], O_RDONLY
);
356 e1
= fd1
>= 0 ? 0 : errno
;
357 fd2
= open(argv
[i
+1], O_RDONLY
);
358 e2
= fd2
>= 0 ? 0 : errno
;
359 if (e1
== EACCES
|| e2
== EACCES
)
360 errorexit(403, "Forbidden", NULL
);
361 if (e1
== ENOENT
|| e1
== ENOTDIR
|| e2
== ENOENT
|| e2
== ENOTDIR
)
362 errorexit(404, "Not Found", NULL
);
363 e1
= fstatfunc(fd1
, &f1
) ? errno
: 0;
364 e2
= fstatfunc(fd2
, &f2
) ? errno
: 0;
366 errorexit(500, "Internal Server Error", NULL
);
367 if (!S_ISREG(f1
.st_mode
) || !S_ISREG(f2
.st_mode
))
368 errorexit(500, "Internal Server Error", NULL
);
373 else if (f1
.st_mtime
>= f2
.st_mtime
)
380 sprintf(etag
, "\"%llx_%llx-%llx-%llx\"", (unsigned long long)f1
.st_ino
,
381 (unsigned long long)f2
.st_ino
, tl
, (unsigned long long)lm
* 1000000U);
386 printf("%s\n", etag
);
390 if (hir
&& *hir
&& strcmp(etag
, hir
))
394 error416(lm
, etag
, tl
); /* Range: not allowed on zero length content */
397 /* Only one range may be specified and it must be bytes */
398 /* with a 2^64 value we could have "Range: bytes = 20-digit - 20-digit" */
400 int s
= sscanf(hr
, " %*[Bb]%*[Yy]%*[Tt]%*[Ee]%*[Ss] = %n", &pos
);
401 if (s
!= 0 || pos
< 6 || strchr(hr
, ','))
402 errorexit(400, "Bad Request", NULL
);
407 /* It's a request for the trailing part */
409 errorexit(400, "Bad Request", NULL
);
411 s
= sscanf(hr
, " %llu%n", &trail
, &pos
);
412 if (s
!= 1 || pos
< 1 || hr
[pos
])
413 errorexit(400, "Bad Request", NULL
);
414 if (!trail
|| trail
> tl
)
415 error416(lm
, etag
, tl
);
420 s
= sscanf(hr
, "%llu - %n", &r1
, &pos
);
421 if (s
!= 1 || pos
< 2)
422 errorexit(400, "Bad Request", NULL
);
426 errorexit(400, "Bad Request", NULL
);
428 s
= sscanf(hr
, "%llu %n", &r2
, &pos
);
429 if (s
!= 1 || pos
< 1 || hr
[pos
])
430 errorexit(400, "Bad Request", NULL
);
434 if (r1
> r2
|| r2
>= tl
)
435 error416(lm
, etag
, tl
);
438 length
= r2
- r1
+ 1;
444 emithdrs(ct
, fn
, expdays
, lm
, etag
, tl
, hr
?1:0, r1
, r2
);
447 if (strcmp(rm
, "HEAD")) {
449 bignum dl
= l1
- start
;
450 if (dl
> length
) dl
= length
;
451 dumpfile(fd1
, start
, dl
);
455 if (length
&& start
>= l1
) {
459 if (dl
> length
) dl
= length
;
460 dumpfile(fd2
, start
, dl
);