Updated changelog.
[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[i] < 32) || (buf[i] >= 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 sendwhole(int fd, struct stat *sb, const char *contype, int head)
145 {
146     printf("HTTP/1.1 200 OK\n");
147     printf("Content-Type: %s\n", contype);
148     printf("Content-Length: %ji\n", (intmax_t)sb->st_size);
149     printf("Last-Modified: %s\n", fmthttpdate(sb->st_mtime));
150     printf("Date: %s\n", fmthttpdate(time(NULL)));
151     printf("\n");
152     fflush(stdout);
153     if(!head)
154         passdata(fd, 1, -1);
155 }
156
157 static void sendrange(int fd, struct stat *sb, const char *contype, char *spec, int head)
158 {
159     char buf[strlen(spec) + 1];
160     char *p, *e;
161     off_t start, end;
162     
163     if(strncmp(spec, "bytes=", 6))
164         goto error;
165     strcpy(buf, spec + 6);
166     if((p = strchr(buf, '-')) == NULL)
167         goto error;
168     if(p == buf) {
169         if(!p[1])
170             goto error;
171         end = sb->st_size;
172         start = end - strtoll(p + 1, &e, 10);
173         if(*e)
174             goto error;
175         if(start < 0)
176             start = 0;
177     } else {
178         *(p++) = 0;
179         start = strtoll(buf, &e, 10);
180         if(*e)
181             goto error;
182         if(*p) {
183             end = strtoll(p, &e, 10) + 1;
184             if(*e)
185                 goto error;
186         } else {
187             end = sb->st_size;
188         }
189     }
190     if(start >= sb->st_size) {
191         printf("HTTP/1.1 416 Not satisfiable\n");
192         printf("Content-Range: */%ji\n", (intmax_t)sb->st_size);
193         printf("Content-Length: 0\n");
194         printf("Last-Modified: %s\n", fmthttpdate(sb->st_mtime));
195         printf("Date: %s\n", fmthttpdate(time(NULL)));
196         printf("\n");
197         return;
198     }
199     if((start < 0) || (start >= end))
200         goto error;
201     if(end > sb->st_size)
202         end = sb->st_size;
203     errno = 0;
204     if(lseek(fd, start, SEEK_SET) != start) {
205         simpleerror(1, 500, "Internal Error", "Could not seek properly to beginning of requested byte range.");
206         flog(LOG_ERR, "sendfile: could not seek properly when serving partial content: %s", strerror(errno));
207         exit(1);
208     }
209     printf("HTTP/1.1 206 Partial content\n");
210     printf("Content-Range: bytes %ji-%ji/%ji\n", (intmax_t)start, (intmax_t)(end - 1), (intmax_t)sb->st_size);
211     printf("Content-Length: %ji\n", (intmax_t)(end - start));
212     printf("Content-Type: %s\n", contype);
213     printf("Last-Modified: %s\n", fmthttpdate(sb->st_mtime));
214     printf("Date: %s\n", fmthttpdate(time(NULL)));
215     printf("\n");
216     fflush(stdout);
217     if(!head)
218         passdata(fd, 1, end - start);
219     return;
220     
221 error:
222     sendwhole(fd, sb, contype, head);
223 }
224
225 static void usage(void)
226 {
227     flog(LOG_ERR, "usage: sendfile [-c CONTENT-TYPE] [-f FILE] METHOD URL REST");
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, ignrest;
236     const char *contype;
237     
238     setlocale(LC_ALL, "");
239     contype = NULL;
240     file = NULL;
241     ignrest = 0;
242     while((c = getopt(argc, argv, "c:f:")) >= 0) {
243         switch(c) {
244         case 'c':
245             contype = optarg;
246             break;
247         case 'f':
248             file = optarg;
249             ignrest = 1;
250             break;
251         default:
252             usage();
253             exit(1);
254             break;
255         }
256     }
257     if(argc - optind < 3) {
258         usage();
259         exit(1);
260     }
261     if((file == NULL) && ((file = getenv("REQ_X_ASH_FILE")) == NULL)) {
262         flog(LOG_ERR, "sendfile: needs to be called with either the X-Ash-File header or the -f option");
263         exit(1);
264     }
265     if(!ignrest && *argv[optind + 2]) {
266         simpleerror(1, 404, "Not Found", "The requested URL has no corresponding resource.");
267         exit(0);
268     }
269     if(stat(file, &sb) || ((fd = open(file, O_RDONLY)) < 0)) {
270         flog(LOG_ERR, "sendfile: could not stat input file %s: %s", file, strerror(errno));
271         simpleerror(1, 500, "Internal Error", "The server could not access its own data.");
272         exit(1);
273     }
274     if(!strcasecmp(argv[optind], "get")) {
275         ishead = 0;
276     } else if(!strcasecmp(argv[optind], "head")) {
277         ishead = 1;
278     } else {
279         simpleerror(1, 405, "Method not allowed", "The requested method is not defined for this resource.");
280         return(0);
281     }
282     if(contype == NULL) {
283         if((hdr = getenv("REQ_X_ASH_CONTENT_TYPE")) != NULL)
284             contype = hdr;
285         else
286             contype = getmimetype(file, &sb);
287     }
288     contype = ckctype(contype);
289     
290     checkcache(file, &sb);
291     
292     if((hdr = getenv("REQ_RANGE")) != NULL)
293         sendrange(fd, &sb, contype, hdr, ishead);
294     else
295         sendwhole(fd, &sb, contype, ishead);
296     return(0);
297 }