Commit | Line | Data |
---|---|---|
a0ef58b1 FT |
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 | #include <sys/socket.h> | |
32 | ||
33 | #ifdef HAVE_CONFIG_H | |
34 | #include <config.h> | |
35 | #endif | |
36 | #include <utils.h> | |
37 | #include <log.h> | |
38 | #include <req.h> | |
39 | #include <resp.h> | |
40 | #include <mt.h> | |
41 | #include <mtio.h> | |
42 | ||
43 | #ifdef HAVE_XATTR | |
44 | #include <attr/xattr.h> | |
45 | #endif | |
46 | ||
47 | static magic_t cookie; | |
48 | ||
49 | static char *attrmimetype(char *file) | |
50 | { | |
51 | #ifdef HAVE_XATTR | |
52 | char buf[1024]; | |
53 | int i; | |
54 | ssize_t sz; | |
55 | ||
56 | if((sz = getxattr(file, "user.ash-mime-type", buf, sizeof(buf) - 1)) > 0) | |
57 | goto found; | |
58 | if((sz = getxattr(file, "user.mime-type", buf, sizeof(buf) - 1)) > 0) | |
59 | goto found; | |
60 | if((sz = getxattr(file, "user.mime_type", buf, sizeof(buf) - 1)) > 0) | |
61 | goto found; | |
62 | if((sz = getxattr(file, "user.Content-Type", buf, sizeof(buf) - 1)) > 0) | |
63 | goto found; | |
64 | return(NULL); | |
65 | found: | |
66 | for(i = 0; i < sz; i++) { | |
67 | if((buf[i] < 32) || (buf[i] >= 128)) | |
68 | return(NULL); | |
69 | } | |
70 | buf[sz] = 0; | |
71 | return(sstrdup(buf)); | |
72 | #else | |
73 | return(NULL); | |
74 | #endif | |
75 | } | |
76 | ||
77 | static char *getmimetype(char *file, struct stat *sb) | |
78 | { | |
79 | char *ret; | |
80 | const char *cret; | |
81 | ||
82 | if((ret = attrmimetype(file)) != NULL) | |
83 | return(ret); | |
84 | if((cret = magic_file(cookie, file)) != NULL) | |
85 | return(sstrdup(cret)); | |
86 | return(sstrdup("application/octet-stream")); | |
87 | } | |
88 | ||
89 | /* XXX: This could be made far better and check for other attributes | |
90 | * and stuff, but not now. */ | |
91 | static char *ckctype(char *ctype) | |
92 | { | |
93 | char *buf; | |
94 | ||
95 | if(!strncmp(ctype, "text/", 5) && (strchr(ctype, ';') == NULL)) { | |
96 | buf = sprintf2("%s; charset=%s", ctype, nl_langinfo(CODESET)); | |
97 | free(ctype); | |
98 | return(buf); | |
99 | } | |
100 | return(ctype); | |
101 | } | |
102 | ||
103 | static int checkcache(struct hthead *req, FILE *out, char *file, struct stat *sb) | |
104 | { | |
105 | char *hdr; | |
106 | ||
107 | if((hdr = getheader(req, "If-Modified-Since")) != NULL) { | |
108 | if(parsehttpdate(hdr) < sb->st_mtime) | |
109 | return(0); | |
110 | fprintf(out, "HTTP/1.1 304 Not Modified\n"); | |
111 | fprintf(out, "Date: %s\n", fmthttpdate(time(NULL))); | |
112 | fprintf(out, "Content-Length: 0\n"); | |
113 | fprintf(out, "\n"); | |
114 | return(1); | |
115 | } | |
116 | return(0); | |
117 | } | |
118 | ||
119 | static off_t passdata(FILE *in, FILE *out, off_t max) | |
120 | { | |
121 | size_t read; | |
122 | off_t total; | |
123 | char buf[8192]; | |
124 | ||
125 | total = 0; | |
126 | while(!feof(in) && ((max < 0) || (total < max))) { | |
127 | read = sizeof(buf); | |
128 | if(max >= 0) | |
129 | read = min(max - total, read); | |
130 | read = fread(buf, 1, read, in); | |
131 | if(ferror(in)) | |
132 | return(-1); | |
133 | if(fwrite(buf, 1, read, out) != read) | |
134 | return(-1); | |
135 | total += read; | |
136 | } | |
137 | return(total); | |
138 | } | |
139 | ||
140 | static void sendwhole(struct hthead *req, FILE *out, FILE *sfile, struct stat *sb, char *contype, int head) | |
141 | { | |
142 | fprintf(out, "HTTP/1.1 200 OK\n"); | |
143 | fprintf(out, "Content-Type: %s\n", contype); | |
144 | fprintf(out, "Content-Length: %ji\n", (intmax_t)sb->st_size); | |
145 | fprintf(out, "Last-Modified: %s\n", fmthttpdate(sb->st_mtime)); | |
146 | fprintf(out, "Date: %s\n", fmthttpdate(time(NULL))); | |
147 | fprintf(out, "\n"); | |
148 | if(!head) | |
149 | passdata(sfile, out, -1); | |
150 | } | |
151 | ||
152 | static void sendrange(struct hthead *req, FILE *out, FILE *sfile, struct stat *sb, char *contype, char *spec, int head) | |
153 | { | |
154 | char buf[strlen(spec) + 1]; | |
155 | char *p, *e; | |
156 | off_t start, end; | |
157 | ||
158 | if(strncmp(spec, "bytes=", 6)) | |
159 | goto error; | |
160 | strcpy(buf, spec + 6); | |
161 | if((p = strchr(buf, '-')) == NULL) | |
162 | goto error; | |
163 | if(p == buf) { | |
164 | if(!p[1]) | |
165 | goto error; | |
166 | end = sb->st_size; | |
167 | start = end - strtoll(p + 1, &e, 10); | |
168 | if(*e) | |
169 | goto error; | |
170 | if(start < 0) | |
171 | start = 0; | |
172 | } else { | |
173 | *(p++) = 0; | |
174 | start = strtoll(buf, &e, 10); | |
175 | if(*e) | |
176 | goto error; | |
177 | if(*p) { | |
178 | end = strtoll(p, &e, 10) + 1; | |
179 | if(*e) | |
180 | goto error; | |
181 | } else { | |
182 | end = sb->st_size; | |
183 | } | |
184 | } | |
185 | if(start >= sb->st_size) { | |
186 | fprintf(out, "HTTP/1.1 416 Not satisfiable\n"); | |
187 | fprintf(out, "Content-Range: */%ji\n", (intmax_t)sb->st_size); | |
188 | fprintf(out, "Content-Length: 0\n"); | |
189 | fprintf(out, "Last-Modified: %s\n", fmthttpdate(sb->st_mtime)); | |
190 | fprintf(out, "Date: %s\n", fmthttpdate(time(NULL))); | |
191 | fprintf(out, "\n"); | |
192 | return; | |
193 | } | |
194 | if((start < 0) || (start >= end)) | |
195 | goto error; | |
196 | if(end > sb->st_size) | |
197 | end = sb->st_size; | |
198 | errno = 0; | |
199 | if(fseeko(sfile, start, SEEK_SET)) { | |
200 | simpleerror2(out, 500, "Internal Error", "Could not seek properly to beginning of requested byte range."); | |
201 | flog(LOG_ERR, "sendfile: could not seek properly when serving partial content: %s", strerror(errno)); | |
202 | return; | |
203 | } | |
204 | fprintf(out, "HTTP/1.1 206 Partial content\n"); | |
205 | fprintf(out, "Content-Range: bytes %ji-%ji/%ji\n", (intmax_t)start, (intmax_t)(end - 1), (intmax_t)sb->st_size); | |
206 | fprintf(out, "Content-Length: %ji\n", (intmax_t)(end - start)); | |
207 | fprintf(out, "Content-Type: %s\n", contype); | |
208 | fprintf(out, "Last-Modified: %s\n", fmthttpdate(sb->st_mtime)); | |
209 | fprintf(out, "Date: %s\n", fmthttpdate(time(NULL))); | |
210 | fprintf(out, "\n"); | |
211 | if(!head) | |
212 | passdata(sfile, out, end - start); | |
213 | return; | |
214 | ||
215 | error: | |
216 | sendwhole(req, out, sfile, sb, contype, head); | |
217 | } | |
218 | ||
219 | static void serve(struct muth *muth, va_list args) | |
220 | { | |
221 | vavar(struct hthead *, req); | |
222 | vavar(int, fd); | |
223 | FILE *out, *sfile; | |
224 | int ishead; | |
225 | char *file, *contype, *hdr; | |
226 | struct stat sb; | |
227 | ||
228 | sfile = NULL; | |
229 | contype = NULL; | |
b71ad67f | 230 | out = mtstdopen(fd, 1, 60, "r+", NULL); |
a0ef58b1 FT |
231 | |
232 | if((file = getheader(req, "X-Ash-File")) == NULL) { | |
233 | flog(LOG_ERR, "psendfile: needs to be called with the X-Ash-File header"); | |
234 | simpleerror2(out, 500, "Internal Error", "The server is incorrectly configured."); | |
235 | goto out; | |
236 | } | |
237 | if(*req->rest) { | |
238 | simpleerror2(out, 404, "Not Found", "The requested URL has no corresponding resource."); | |
239 | goto out; | |
240 | } | |
107712e3 | 241 | if(((sfile = fopen(file, "r")) == NULL) || fstat(fileno(sfile), &sb)) { |
a0ef58b1 FT |
242 | flog(LOG_ERR, "psendfile: could not stat input file %s: %s", file, strerror(errno)); |
243 | simpleerror2(out, 500, "Internal Error", "The server could not access its own data."); | |
244 | goto out; | |
245 | } | |
246 | if(!strcasecmp(req->method, "get")) { | |
247 | ishead = 0; | |
248 | } else if(!strcasecmp(req->method, "head")) { | |
249 | ishead = 1; | |
250 | } else { | |
251 | simpleerror2(out, 405, "Method not allowed", "The requested method is not defined for this resource."); | |
252 | goto out; | |
253 | } | |
254 | if((hdr = getheader(req, "X-Ash-Content-Type")) == NULL) | |
255 | contype = getmimetype(file, &sb); | |
256 | else | |
257 | contype = sstrdup(hdr); | |
258 | contype = ckctype(contype); | |
259 | ||
260 | if(checkcache(req, out, file, &sb)) | |
261 | goto out; | |
262 | ||
263 | if((hdr = getheader(req, "Range")) != NULL) | |
264 | sendrange(req, out, sfile, &sb, contype, hdr, ishead); | |
265 | else | |
266 | sendwhole(req, out, sfile, &sb, contype, ishead); | |
267 | ||
268 | out: | |
269 | if(sfile != NULL) | |
270 | fclose(sfile); | |
8f18d703 FT |
271 | if(contype != NULL) |
272 | free(contype); | |
a0ef58b1 FT |
273 | fclose(out); |
274 | freehthead(req); | |
275 | } | |
276 | ||
277 | static void listenloop(struct muth *muth, va_list args) | |
278 | { | |
279 | vavar(int, lfd); | |
280 | int fd; | |
281 | struct hthead *req; | |
282 | ||
283 | while(1) { | |
284 | block(lfd, EV_READ, 0); | |
285 | if((fd = recvreq(lfd, &req)) < 0) { | |
286 | if(errno != 0) | |
287 | flog(LOG_ERR, "recvreq: %s", strerror(errno)); | |
288 | break; | |
289 | } | |
290 | mustart(serve, req, fd); | |
291 | } | |
292 | } | |
293 | ||
294 | static void sigterm(int sig) | |
295 | { | |
296 | shutdown(0, SHUT_RDWR); | |
297 | } | |
298 | ||
299 | static void usage(FILE *out) | |
300 | { | |
301 | fprintf(out, "usage: psendfile [-h]\n"); | |
302 | } | |
303 | ||
304 | int main(int argc, char **argv) | |
305 | { | |
306 | int c; | |
307 | ||
308 | setlocale(LC_ALL, ""); | |
309 | while((c = getopt(argc, argv, "h")) >= 0) { | |
310 | switch(c) { | |
311 | case 'h': | |
312 | usage(stdout); | |
313 | exit(0); | |
314 | default: | |
315 | usage(stderr); | |
316 | exit(1); | |
317 | } | |
318 | } | |
319 | cookie = magic_open(MAGIC_MIME_TYPE | MAGIC_SYMLINK); | |
320 | magic_load(cookie, NULL); | |
321 | mustart(listenloop, 0); | |
322 | signal(SIGINT, sigterm); | |
323 | signal(SIGTERM, sigterm); | |
324 | ioloop(); | |
325 | return(0); | |
326 | } |