sendfile: Added support for partial content a la HTTP 206.
[ashd.git] / src / sendfile.c
1 /*
2     ashd - A Sane HTTP Daemon
3     Copyright (C) 2008  Fredrik Tolf <fredrik@dolda2000.com>
4
5     This program is free software: you can redistribute it and/or modify
6     it under the terms of the GNU General Public License as published by
7     the Free Software Foundation, either version 3 of the License, or
8     (at your option) any later version.
9
10     This program is distributed in the hope that it will be useful,
11     but WITHOUT ANY WARRANTY; without even the implied warranty of
12     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13     GNU General Public License for more details.
14
15     You should have received a copy of the GNU General Public License
16     along with this program.  If not, see <http://www.gnu.org/licenses/>.
17 */
18
19 #include <stdlib.h>
20 #include <string.h>
21 #include <stdio.h>
22 #include <unistd.h>
23 #include <fcntl.h>
24 #include <errno.h>
25 #include <sys/stat.h>
26 #include <stdint.h>
27 #include <time.h>
28 #include <magic.h>
29 #include <locale.h>
30 #include <langinfo.h>
31
32 #ifdef HAVE_CONFIG_H
33 #include <config.h>
34 #endif
35 #include <utils.h>
36 #include <log.h>
37 #include <resp.h>
38
39 #ifdef HAVE_XATTR
40 #include <attr/xattr.h>
41 #endif
42
43 static magic_t cookie = NULL;
44
45 static void passdata(int in, int out, off_t maxlen)
46 {
47     int ret, len, off;
48     char *buf;
49     
50     buf = smalloc(65536);
51     while(1) {
52         len = 65536;
53         if((maxlen > 0) && (len > maxlen))
54             len = maxlen;
55         len = read(in, buf, len);
56         if(len < 0) {
57             flog(LOG_ERR, "sendfile: could not read input: %s", strerror(errno));
58             break;
59         }
60         if(len == 0)
61             break;
62         for(off = 0; off < len; off += ret) {
63             ret = write(out, buf + off, len - off);
64             if(ret < 0) {
65                 flog(LOG_ERR, "sendfile: could not write output: %s", strerror(errno));
66                 break;
67             }
68         }
69         if(maxlen > 0) {
70             if((maxlen -= len) <= 0)
71                 break;
72         }
73     }
74     free(buf);
75 }
76
77 static char *attrmimetype(char *file)
78 {
79 #ifdef HAVE_XATTR
80     static char buf[1024];
81     int i;
82     ssize_t sz;
83     
84     if((sz = getxattr(file, "user.ash-mime-type", buf, sizeof(buf) - 1)) > 0)
85         goto found;
86     if((sz = getxattr(file, "user.mime-type", buf, sizeof(buf) - 1)) > 0)
87         goto found;
88     if((sz = getxattr(file, "user.mime_type", buf, sizeof(buf) - 1)) > 0)
89         goto found;
90     if((sz = getxattr(file, "user.Content-Type", buf, sizeof(buf) - 1)) > 0)
91         goto found;
92     return(NULL);
93 found:
94     for(i = 0; i < sz; i++) {
95         if((buf[sz] < 32) || (buf[sz] >= 128))
96             return(NULL);
97     }
98     buf[sz] = 0;
99     return(buf);
100 #else
101     return(NULL);
102 #endif
103 }
104
105 static const char *getmimetype(char *file, struct stat *sb)
106 {
107     const char *ret;
108     
109     if((ret = attrmimetype(file)) != NULL)
110         return(ret);
111     if(cookie == NULL) {
112         cookie = magic_open(MAGIC_MIME_TYPE | MAGIC_SYMLINK);
113         magic_load(cookie, NULL);
114     }
115     if((ret = magic_file(cookie, file)) != NULL)
116         return(ret);
117     return("application/octet-stream");
118 }
119
120 /* XXX: This could be made far better and check for other attributes
121  * and stuff, but not now. */
122 static const char *ckctype(const char *ctype)
123 {
124     if(!strncmp(ctype, "text/", 5) && (strchr(ctype, ';') == NULL))
125         return(sprintf2("%s; charset=%s", ctype, nl_langinfo(CODESET)));
126     return(ctype);
127 }
128
129 static void checkcache(char *file, struct stat *sb)
130 {
131     char *hdr;
132     
133     if((hdr = getenv("REQ_IF_MODIFIED_SINCE")) != NULL) {
134         if(parsehttpdate(hdr) < sb->st_mtime)
135             return;
136         printf("HTTP/1.1 304 Not Modified\n");
137         printf("Date: %s\n", fmthttpdate(time(NULL)));
138         printf("Content-Length: 0\n");
139         printf("\n");
140         exit(0);
141     }
142 }
143
144 static void usage(void)
145 {
146     flog(LOG_ERR, "usage: sendfile [-c CONTENT-TYPE] METHOD URL REST");
147 }
148
149 static void sendwhole(int fd, struct stat *sb, const char *contype, int head)
150 {
151     printf("HTTP/1.1 200 OK\n");
152     printf("Content-Type: %s\n", contype);
153     printf("Content-Length: %ji\n", (intmax_t)sb->st_size);
154     printf("Last-Modified: %s\n", fmthttpdate(sb->st_mtime));
155     printf("Date: %s\n", fmthttpdate(time(NULL)));
156     printf("\n");
157     fflush(stdout);
158     if(!head)
159         passdata(fd, 1, -1);
160 }
161
162 static void sendrange(int fd, struct stat *sb, const char *contype, char *spec, int head)
163 {
164     char buf[strlen(spec) + 1];
165     char *p, *e;
166     off_t start, end;
167     
168     if(strncmp(spec, "bytes=", 6))
169         goto error;
170     strcpy(buf, spec + 6);
171     if((p = strchr(buf, '-')) == NULL)
172         goto error;
173     if(p == buf) {
174         if(!p[1])
175             goto error;
176         end = sb->st_size;
177         start = end - strtoll(p + 1, &e, 10);
178         if(*e)
179             goto error;
180         if(start < 0)
181             start = 0;
182     } else {
183         *(p++) = 0;
184         start = strtoll(buf, &e, 10);
185         if(*e)
186             goto error;
187         if(*p) {
188             end = strtoll(p, &e, 10) + 1;
189             if(*e)
190                 goto error;
191         } else {
192             end = sb->st_size;
193         }
194     }
195     if(start >= sb->st_size) {
196         printf("HTTP/1.1 416 Not satisfiable\n");
197         printf("Content-Range: */%ji\n", (intmax_t)sb->st_size);
198         printf("Content-Length: 0\n");
199         printf("Last-Modified: %s\n", fmthttpdate(sb->st_mtime));
200         printf("Date: %s\n", fmthttpdate(time(NULL)));
201         printf("\n");
202         return;
203     }
204     if((start < 0) || (start >= end))
205         goto error;
206     if(end > sb->st_size)
207         end = sb->st_size;
208     errno = 0;
209     if(lseek(fd, start, SEEK_SET) != start) {
210         simpleerror(1, 500, "Internal Error", "Could not seek properly to beginning of requested byte range.");
211         flog(LOG_ERR, "sendfile: could not seek properly when serving partial content: %s", strerror(errno));
212         exit(1);
213     }
214     printf("HTTP/1.1 206 Partial content\n");
215     printf("Content-Range: bytes %ji-%ji/%ji\n", (intmax_t)start, (intmax_t)(end - 1), (intmax_t)sb->st_size);
216     printf("Content-Length: %ji\n", (intmax_t)(end - start));
217     printf("Content-Type: %s\n", contype);
218     printf("Last-Modified: %s\n", fmthttpdate(sb->st_mtime));
219     printf("Date: %s\n", fmthttpdate(time(NULL)));
220     printf("\n");
221     fflush(stdout);
222     if(!head)
223         passdata(fd, 1, end - start);
224     return;
225     
226 error:
227     sendwhole(fd, sb, contype, head);
228 }
229
230 int main(int argc, char **argv)
231 {
232     int c;
233     char *file, *hdr;
234     struct stat sb;
235     int fd, ishead;
236     const char *contype;
237     
238     setlocale(LC_ALL, "");
239     contype = NULL;
240     while((c = getopt(argc, argv, "c:")) >= 0) {
241         switch(c) {
242         case 'c':
243             contype = optarg;
244             break;
245         default:
246             usage();
247             exit(1);
248             break;
249         }
250     }
251     if(argc - optind < 3) {
252         usage();
253         exit(1);
254     }
255     if((file = getenv("REQ_X_ASH_FILE")) == NULL) {
256         flog(LOG_ERR, "sendfile: needs to be called with the X-Ash-File header");
257         exit(1);
258     }
259     if(*argv[optind + 2]) {
260         simpleerror(1, 404, "Not Found", "The requested URL has no corresponding resource.");
261         exit(0);
262     }
263     if(stat(file, &sb) || ((fd = open(file, O_RDONLY)) < 0)) {
264         flog(LOG_ERR, "sendfile: could not stat input file %s: %s", file, strerror(errno));
265         simpleerror(1, 500, "Internal Error", "The server could not access its own data.");
266         exit(1);
267     }
268     if(contype == NULL)
269         contype = getmimetype(file, &sb);
270     contype = ckctype(contype);
271     
272     checkcache(file, &sb);
273     
274     ishead = !strcasecmp(argv[optind], "head");
275     if((hdr = getenv("REQ_RANGE")) != NULL)
276         sendrange(fd, &sb, contype, hdr, ishead);
277     else
278         sendwhole(fd, &sb, contype, ishead);
279     return(0);
280 }