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] | [-c <content-type>] [-e <days>]) 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 If --etag is given then no other options are allowed. If -c <content-type>
37 is given then the specified content type will be used as-is for the returned
38 item. If -e <days> is given then a cache-control and expires header will
39 be output with the expiration set that many days into the future.
41 Other CGI parameters MUST be passed as environment variables in particular
42 REQUEST_METHOD MUST be set and to request a range, HTTP_RANGE MUST be set.
43 HTTP_IF_RANGE MAY be set. No other environment variables are examined.
45 Exit code 0 for CGI success (Status: header etc. output)
46 Exit code 1 for no REQUEST_METHOD.
47 Exit code 2 for file1 and/or file2 not given or wrong type or bad option.
48 Exit code 3 for --etag mode and file1 and/or file2 could not be opened.
50 If file1 and/or file2 is not found a 404 status will be output.
52 Normally a front end script will begin processing the initial CGI request
53 from the web server and then pass it on to this utility as appropriate.
55 If a "Range:" header is present and a non-empty "If-Range:" header is also
56 present then if the value in the "If-Range:" header does not exactly match
57 the computed "ETag:" value then the "Range:" header will be silently ignored.
60 #undef _FILE_OFFSET_BITS
61 #define _FILE_OFFSET_BITS 64
62 #include <sys/types.h>
73 #include <AvailabilityMacros.h>
74 #ifndef MAC_OS_X_VERSION_10_5
75 #define MAC_OS_X_VERSION_10_5 1050
77 #if MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_5
78 typedef struct stat statrec
;
79 #define fstatfunc(p,b) fstat(p,b)
81 typedef struct stat64 statrec
;
82 #define fstatfunc(p,b) fstat64(p,b)
85 typedef struct stat statrec
;
86 #define fstatfunc(p,b) fstat(p,b)
88 typedef unsigned long long bignum
;
90 static void errorfail_(unsigned code
, const char *status
, const char *extrahdr
)
98 static void errorexit_(unsigned code
, const char *status
, const char *extrahdr
)
100 printf("Status: %u %s\r\n", code
, status
);
101 printf("%s\r\n", "Expires: Fri, 01 Jan 1980 00:00:00 GMT");
102 printf("%s\r\n", "Pragma: no-cache");
103 printf("%s\r\n", "Cache-Control: no-cache,max-age=0,must-revalidate");
104 printf("%s\r\n", "Accept-Ranges: bytes");
106 printf("%s\r\n", extrahdr
);
107 printf("%s\r\n", "Content-Type: text/plain");
108 printf("%s\r\n", "");
109 printf("%s\n", status
);
114 static void emithdrs(const char *ct
, int exp
, time_t lm
, const char *etag
,
115 bignum tl
, int isr
, bignum r1
, bignum r2
)
122 printf("Status: %u %s\r\n", 206, "Partial Content");
124 printf("Status: %u %s\r\n", 416, "Requested Range Not Satisfiable");
126 printf("Status: %u %s\r\n", 200, "OK");
128 time_t epsecs
= time(NULL
);
129 long esecs
= 86400 * exp
;
130 gt
= *gmtime(&epsecs
);
131 strftime(dtstr
, sizeof(dtstr
), "%a, %d %b %Y %H:%M:%S GMT", >
);
132 printf("Date: %s\r\n", dtstr
);
134 gt
= *gmtime(&epsecs
);
135 strftime(dtstr
, sizeof(dtstr
), "%a, %d %b %Y %H:%M:%S GMT", >
);
136 printf("Expires: %s\r\n", dtstr
);
137 printf("Cache-Control: public,max-age=%ld\r\n", esecs
);
139 printf("%s\r\n", "Expires: Fri, 01 Jan 1980 00:00:00 GMT");
140 printf("%s\r\n", "Pragma: no-cache");
141 printf("%s\r\n", "Cache-Control: no-cache,max-age=0,must-revalidate");
143 printf("%s\r\n", "Accept-Ranges: bytes");
145 strftime(dtstr
, sizeof(dtstr
), "%a, %d %b %Y %H:%M:%S GMT", >
);
146 printf("Last-Modified: %s\r\n", dtstr
);
148 printf("ETag: %s\r\n", etag
);
150 printf("Content-Length: %llu\r\n", tl
);
151 } else if (isr
> 0) {
152 printf("Content-Length: %llu\r\n", r2
- r1
+ 1);
153 printf("Content-Range: bytes %llu-%llu/%llu\r\n", r1
, r2
, tl
);
155 printf("Content-Range: bytes */%llu\r\n", tl
);
159 ct
= "application/octet-stream";
160 printf("Content-Type: %s\r\n", ct
);
161 printf("%s\r\n", "Vary: Accept-Encoding");
163 printf("%s\r\n%s\n", "Content-Type: text/plain",
164 "Requested Range Not Satisfiable");
166 printf("%s\r\n", "");
169 static void error416(time_t lm
, const char *etag
, bignum tl
)
171 emithdrs(NULL
, -1, lm
, etag
, tl
, -1, 0, 0);
176 static void die(const char *msg
)
178 fprintf(stderr
, "%s\n", msg
);
183 static void readx(int fd
, void *buf
, size_t count
)
185 char *buff
= (char *)buf
;
187 while (count
&& (!err
|| err
== EINTR
|| err
== EAGAIN
|| err
== EWOULDBLOCK
)) {
188 ssize_t amt
= read(fd
, buff
, count
);
197 count
-= (size_t)amt
;
201 die("failed reading file (error)");
203 die("failed reading file (EOF)");
207 static void writex(int fd
, const void *buf
, size_t count
)
209 const char *buff
= (const char *)buf
;
211 while (count
&& (!err
|| err
== EINTR
|| err
== EAGAIN
|| err
== EWOULDBLOCK
)) {
212 ssize_t amt
= write(fd
, buff
, count
);
221 count
-= (size_t)amt
;
225 die("failed writing file (error)");
227 die("failed writing file (EOF)");
232 static char dumpbuff
[1U << SIZEPWR
];
233 void dumpfile(int fd
, bignum start
, bignum len
)
235 off_t loc
= lseek(fd
, (off_t
)start
, SEEK_SET
);
237 if (loc
!= (off_t
)start
)
239 if (start
& ((bignum
)(sizeof(dumpbuff
) - 1)))
240 maxread
= sizeof(dumpbuff
) - (size_t)(start
& ((bignum
)(sizeof(dumpbuff
) - 1)));
242 maxread
= sizeof(dumpbuff
);
244 size_t cnt
= len
> (bignum
)maxread
? maxread
: (size_t)len
;
245 readx(fd
, dumpbuff
, cnt
);
246 writex(STDOUT_FILENO
, dumpbuff
, cnt
);
248 maxread
= sizeof(dumpbuff
);
253 int main(int argc
, char *argv
[])
255 int isetag
= argc
== 4 && !strcmp(argv
[1], "--etag");
256 void (*errorexit
)(unsigned,const char *,const char *) =
257 isetag
? errorfail_
: errorexit_
;
262 bignum start
, length
;
264 const char *rm
= !isetag
? getenv("REQUEST_METHOD") : NULL
;
265 const char *hr
= !isetag
? getenv("HTTP_RANGE") : NULL
;
266 const char *hir
= hr
? getenv("HTTP_IF_RANGE") : NULL
;
267 const char *ct
= NULL
;
269 /* "inode_inode-size-time_t_micros" each in hex up to 8 bytes gives */
270 /* "16bytes_16bytes-16bytes-16bytes" plus NUL = 70 bytes (including "") */
272 int fd1
= -1, fd2
= -1;
279 while ((ch
= getopt(argc
, argv
, "c:e:")) != -1) {
287 if (sscanf(optarg
, "%i%n", &v
, &n
) != 1 || n
!= (int)strlen(optarg
))
296 if (argc
- optind
!= 2)
303 if (!isetag
&& strcmp(rm
, "GET") && strcmp(rm
, "HEAD"))
304 errorexit(405, "Method Not Allowed", "Allow: GET,HEAD");
306 fd1
= open(argv
[i
], O_RDONLY
);
307 e1
= fd1
>= 0 ? 0 : errno
;
308 fd2
= open(argv
[i
+1], O_RDONLY
);
309 e2
= fd2
>= 0 ? 0 : errno
;
310 if (e1
== EACCES
|| e2
== EACCES
)
311 errorexit(403, "Forbidden", NULL
);
312 if (e1
== ENOENT
|| e1
== ENOTDIR
|| e2
== ENOENT
|| e2
== ENOTDIR
)
313 errorexit(404, "Not Found", NULL
);
314 e1
= fstatfunc(fd1
, &f1
) ? errno
: 0;
315 e2
= fstatfunc(fd2
, &f2
) ? errno
: 0;
317 errorexit(500, "Internal Server Error", NULL
);
318 if (!S_ISREG(f1
.st_mode
) || !S_ISREG(f2
.st_mode
))
319 errorexit(500, "Internal Server Error", NULL
);
320 if (f1
.st_mtime
>= f2
.st_mtime
)
327 sprintf(etag
, "\"%llx_%llx-%llx-%llx\"", (unsigned long long)f1
.st_ino
,
328 (unsigned long long)f2
.st_ino
, tl
, (unsigned long long)lm
* 1000000U);
333 printf("%s\n", etag
);
337 if (hir
&& *hir
&& strcmp(etag
, hir
))
341 error416(lm
, etag
, tl
); /* Range: not allowed on zero length content */
344 /* Only one range may be specified and it must be bytes */
345 /* with a 2^64 value we could have "Range: bytes = 20-digit - 20-digit" */
347 int s
= sscanf(hr
, " %*[Bb]%*[Yy]%*[Tt]%*[Ee]%*[Ss] = %n", &pos
);
348 if (s
!= 0 || pos
< 6 || strchr(hr
, ','))
349 errorexit(400, "Bad Request", NULL
);
354 /* It's a request for the trailing part */
356 errorexit(400, "Bad Request", NULL
);
358 s
= sscanf(hr
, " %llu%n", &trail
, &pos
);
359 if (s
!= 1 || pos
< 1 || hr
[pos
])
360 errorexit(400, "Bad Request", NULL
);
361 if (!trail
|| trail
> tl
)
362 error416(lm
, etag
, tl
);
367 s
= sscanf(hr
, "%llu - %n", &r1
, &pos
);
368 if (s
!= 1 || pos
< 2)
369 errorexit(400, "Bad Request", NULL
);
373 errorexit(400, "Bad Request", NULL
);
375 s
= sscanf(hr
, "%llu %n", &r2
, &pos
);
376 if (s
!= 1 || pos
< 1 || hr
[pos
])
377 errorexit(400, "Bad Request", NULL
);
381 if (r1
> r2
|| r2
>= tl
)
382 error416(lm
, etag
, tl
);
385 length
= r2
- r1
+ 1;
391 emithdrs(ct
, expdays
, lm
, etag
, tl
, hr
?1:0, r1
, r2
);
394 if (strcmp(rm
, "HEAD")) {
396 bignum dl
= l1
- start
;
397 if (dl
> length
) dl
= length
;
398 dumpfile(fd1
, start
, dl
);
402 if (length
&& start
>= l1
) {
406 if (dl
> length
) dl
= length
;
407 dumpfile(fd2
, start
, dl
);