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