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] 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 Other CGI parameters MUST be passed as environment variables in particular
37 REQUEST_METHOD MUST be set and to request a range, HTTP_RANGE MUST be set.
38 HTTP_IF_RANGE MAY be set. No other environment variables are examined.
40 Exit code 0 for CGI success (Status: header etc. output)
41 Exit code 1 for no REQUEST_METHOD.
42 Exit code 2 for file1 and/or file2 not given or wrong type or bad option.
43 Exit code 3 for --etag mode and file1 and/or file2 could not be opened.
45 If file1 and/or file2 is not found a 404 status will be output.
47 Normally a front end script will begin processing the initial CGI request
48 from the web server and then pass it on to this utility as appropriate.
50 If a "Range:" header is present and a non-empty "If-Range:" header is also
51 present then if the value in the "If-Range:" header does not exactly match
52 the computed "ETag:" value then the "Range:" header will be silently ignored.
55 #undef _FILE_OFFSET_BITS
56 #define _FILE_OFFSET_BITS 64
57 #include <sys/types.h>
68 #include <AvailabilityMacros.h>
69 #ifndef MAC_OS_X_VERSION_10_5
70 #define MAC_OS_X_VERSION_10_5 1050
72 #if MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_5
73 typedef struct stat statrec
;
74 #define fstatfunc(p,b) fstat(p,b)
76 typedef struct stat64 statrec
;
77 #define fstatfunc(p,b) fstat64(p,b)
80 typedef struct stat statrec
;
81 #define fstatfunc(p,b) fstat(p,b)
83 typedef unsigned long long bignum
;
85 static void errorfail_(unsigned code
, const char *status
, const char *extrahdr
)
93 static void errorexit_(unsigned code
, const char *status
, const char *extrahdr
)
95 printf("Status: %u %s\r\n", code
, status
);
96 printf("%s\r\n", "Expires: Fri, 01 Jan 1980 00:00:00 GMT");
97 printf("%s\r\n", "Pragma: no-cache");
98 printf("%s\r\n", "Cache-Control: no-cache, max-age=0, must-revalidate");
99 printf("%s\r\n", "Accept-Ranges: bytes");
101 printf("%s\r\n", extrahdr
);
102 printf("%s\r\n", "Content-Type: text/plain");
103 printf("%s\r\n", "");
104 printf("%s\n", status
);
109 static void emithdrs(time_t lm
, const char *etag
, bignum tl
, int isr
, bignum r1
, bignum r2
)
115 strftime(lmstr
, sizeof(lmstr
), "%a, %d %b %Y %H:%M:%S GMT", >
);
118 printf("Status: %u %s\r\n", 206, "Partial Content");
120 printf("Status: %u %s\r\n", 416, "Requested Range Not Satisfiable");
122 printf("Status: %u %s\r\n", 200, "OK");
123 printf("%s\r\n", "Accept-Ranges: bytes");
124 printf("Last-Modified: %s\r\n", lmstr
);
126 printf("ETag: %s\r\n", etag
);
128 printf("Content-Length: %llu\r\n", tl
);
129 } else if (isr
> 0) {
130 printf("Content-Length: %llu\r\n", r2
- r1
+ 1);
131 printf("Content-Range: bytes %llu-%llu/%llu\r\n", r1
, r2
, tl
);
133 printf("Content-Range: bytes */%llu\r\n", tl
);
136 printf("%s\r\n", "Content-Type: application/octet-stream");
138 printf("%s\r\n%s\n", "Content-Type: text/plain",
139 "Requested Range Not Satisfiable");
140 printf("%s\r\n", "");
143 static void error416(time_t lm
, const char *etag
, bignum tl
)
145 emithdrs(lm
, etag
, tl
, -1, 0, 0);
150 static void die(const char *msg
)
152 fprintf(stderr
, "%s\n", msg
);
157 static void readx(int fd
, void *buf
, size_t count
)
159 char *buff
= (char *)buf
;
161 while (count
&& (!err
|| err
== EINTR
|| err
== EAGAIN
|| err
== EWOULDBLOCK
)) {
162 ssize_t amt
= read(fd
, buff
, count
);
171 count
-= (size_t)amt
;
175 die("failed reading file (error)");
177 die("failed reading file (EOF)");
181 static void writex(int fd
, const void *buf
, size_t count
)
183 const char *buff
= (const char *)buf
;
185 while (count
&& (!err
|| err
== EINTR
|| err
== EAGAIN
|| err
== EWOULDBLOCK
)) {
186 ssize_t amt
= write(fd
, buff
, count
);
195 count
-= (size_t)amt
;
199 die("failed writing file (error)");
201 die("failed writing file (EOF)");
206 static char dumpbuff
[1U << SIZEPWR
];
207 void dumpfile(int fd
, bignum start
, bignum len
)
209 off_t loc
= lseek(fd
, (off_t
)start
, SEEK_SET
);
211 if (loc
!= (off_t
)start
)
213 if (start
& ((bignum
)(sizeof(dumpbuff
) - 1)))
214 maxread
= sizeof(dumpbuff
) - (size_t)(start
& ((bignum
)(sizeof(dumpbuff
) - 1)));
216 maxread
= sizeof(dumpbuff
);
218 size_t cnt
= len
> (bignum
)maxread
? maxread
: (size_t)len
;
219 readx(fd
, dumpbuff
, cnt
);
220 writex(STDOUT_FILENO
, dumpbuff
, cnt
);
222 maxread
= sizeof(dumpbuff
);
227 int main(int argc
, char *argv
[])
229 void (*errorexit
)(unsigned,const char *,const char *) =
230 argc
== 3 ? errorexit_
: errorfail_
;
235 bignum start
, length
;
237 const char *rm
= argc
== 3 ? getenv("REQUEST_METHOD") : NULL
;
238 const char *hr
= argc
== 3 ? getenv("HTTP_RANGE") : NULL
;
239 const char *hir
= hr
? getenv("HTTP_IF_RANGE") : NULL
;
240 /* "inode_inode-size-time_t_micros" each in hex up to 8 bytes gives */
241 /* "16bytes_16bytes-16bytes-16bytes" plus NUL = 70 bytes (including "") */
243 int fd1
= -1, fd2
= -1;
245 if (argc
== 3 && (!rm
|| !*rm
))
248 if (argc
< 3 || argc
> 4)
251 if (strcmp(argv
[1], "--etag"))
256 if (argc
== 3 && strcmp(rm
, "GET") && strcmp(rm
, "HEAD"))
257 errorexit(405, "Method Not Allowed", "Allow: GET,HEAD");
259 fd1
= open(argv
[i
], O_RDONLY
);
260 e1
= fd1
>= 0 ? 0 : errno
;
261 fd2
= open(argv
[i
+1], O_RDONLY
);
262 e2
= fd2
>= 0 ? 0 : errno
;
263 if (e1
== EACCES
|| e2
== EACCES
)
264 errorexit(403, "Forbidden", NULL
);
265 if (e1
== ENOENT
|| e1
== ENOTDIR
|| e2
== ENOENT
|| e2
== ENOTDIR
)
266 errorexit(404, "Not Found", NULL
);
267 e1
= fstatfunc(fd1
, &f1
) ? errno
: 0;
268 e2
= fstatfunc(fd2
, &f2
) ? errno
: 0;
270 errorexit(500, "Internal Server Error", NULL
);
271 if (!S_ISREG(f1
.st_mode
) || !S_ISREG(f2
.st_mode
))
272 errorexit(500, "Internal Server Error", NULL
);
273 if (f1
.st_mtime
>= f2
.st_mtime
)
280 sprintf(etag
, "\"%llx_%llx-%llx-%llx\"", (unsigned long long)f1
.st_ino
,
281 (unsigned long long)f2
.st_ino
, tl
, (unsigned long long)lm
* 1000000U);
286 printf("%s\n", etag
);
290 if (hir
&& *hir
&& strcmp(etag
, hir
))
294 error416(lm
, etag
, tl
); /* Range: not allowed on zero length content */
297 /* Only one range may be specified and it must be bytes */
298 /* with a 2^64 value we could have "Range: bytes = 20-digit - 20-digit" */
300 int s
= sscanf(hr
, " %*[Bb]%*[Yy]%*[Tt]%*[Ee]%*[Ss] = %n", &pos
);
301 if (s
!= 0 || pos
< 6 || strchr(hr
, ','))
302 errorexit(400, "Bad Request", NULL
);
307 /* It's a request for the trailing part */
309 errorexit(400, "Bad Request", NULL
);
311 s
= sscanf(hr
, " %llu%n", &trail
, &pos
);
312 if (s
!= 1 || pos
< 1 || hr
[pos
])
313 errorexit(400, "Bad Request", NULL
);
314 if (!trail
|| trail
> tl
)
315 error416(lm
, etag
, tl
);
320 s
= sscanf(hr
, "%llu - %n", &r1
, &pos
);
321 if (s
!= 1 || pos
< 2)
322 errorexit(400, "Bad Request", NULL
);
326 errorexit(400, "Bad Request", NULL
);
328 s
= sscanf(hr
, "%llu %n", &r2
, &pos
);
329 if (s
!= 1 || pos
< 1 || hr
[pos
])
330 errorexit(400, "Bad Request", NULL
);
334 if (r1
> r2
|| r2
>= tl
)
335 error416(lm
, etag
, tl
);
338 length
= r2
- r1
+ 1;
344 if (!strcmp(rm
, "HEAD")) {
345 emithdrs(lm
, etag
, tl
, hr
?1:0, r1
, r2
);
350 emithdrs(lm
, etag
, tl
, hr
?1:0, r1
, r2
);
354 bignum dl
= l1
- start
;
355 if (dl
> length
) dl
= length
;
356 dumpfile(fd1
, start
, dl
);
360 if (length
&& start
>= l1
) {
364 if (dl
> length
) dl
= length
;
365 dumpfile(fd2
, start
, dl
);