2 * - by Richard W.M. Jones <rich@annexia.org>
4 * This library is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU Library General Public
6 * License as published by the Free Software Foundation; either
7 * version 2 of the License, or (at your option) any later version.
9 * This library is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 * Library General Public License for more details.
14 * You should have received a copy of the GNU Library General Public
15 * License along with this library; if not, write to the Free
16 * Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
18 * $Id: file.c,v 1.15 2003/02/05 23:02:51 rich Exp $
34 #ifdef HAVE_SYS_TYPES_H
35 #include <sys/types.h>
38 #ifdef HAVE_SYS_STAT_H
42 #ifdef HAVE_SYS_MMAN_H
64 #include <pthr_pseudothread.h>
65 #include <pthr_http.h>
66 #include <pthr_iolib.h>
68 #include "process_rq.h"
69 #include "mime_types.h"
77 /* XXX This code doesn't deal with the "If-Modified-Since" header
78 * correctly. It is important to get this fixed in the near future.
80 * Similarly the code should send "Last-Modified" headers.
96 static pool file_pool = 0;
98 #define MAX_MMAP_SIZE (10 * 1024 * 1024)
99 #define MAX_ENTRIES 100
100 #define MAX_SIZE (100 * 1024 * 1024)
102 static int total_size = 0;
103 static int nr_entries = 0;
105 /* This is the list of files (of type struct file_info) which are
106 * currently memory mapped. It is stored in no particular order and
107 * may contain blank entries (where a file has been unmapped for example).
109 static vector file_list = 0;
111 /* This is a list of integers indexing into file_list, stored in LRU
112 * order. Element 0 is the oldest, and larger numbered elements are
113 * younger. New entries are pushed onto the back of this list.
115 static vector lru_list = 0;
117 /* This hash of { device, inode } -> integer maps unique stat information
118 * about files to their offset in the file_list array above.
120 static hash file_hash = 0;
122 static void invalidate_entry (void *);
123 static int quickly_serve_it (process_rq p, const struct file_info *info, const char *mime_type);
124 static int slowly_serve_it (process_rq p, int fd, const char *mime_type);
125 static void expires_header (process_rq p, http_response http_response);
127 /* Initialize structures. */
131 file_pool = new_subpool (global_pool);
132 file_list = new_vector (file_pool, struct file_info);
133 lru_list = new_vector (file_pool, int);
134 file_hash = new_hash (file_pool, struct hash_key, int);
138 file_serve (process_rq p)
141 const char *mime_type = 0;
144 struct file_info info;
147 /* If this file is an executable .so file, and we are allowed to
148 * run .so files from this directory, then it's a shared object
149 * script. Hand it off to exec_so.c to run.
151 if (cfg_get_bool (p->host, p->alias, "exec so", 0) &&
152 prematch (p->pool, p->remainder, re_so, 0) &&
153 (p->statbuf.st_mode & (S_IXUSR | S_IXGRP | S_IXOTH)))
154 return exec_so_file (p);
156 /* If this file is executable, and we are allowed to run files from
157 * this directory, then it's a CGI script. Hand it off to exec.c to
160 if (cfg_get_bool (p->host, p->alias, "exec", 0) &&
161 (p->statbuf.st_mode & (S_IXUSR | S_IXGRP | S_IXOTH)))
162 return exec_file (p);
164 /* Are we permitted to show files in this directory? */
165 if (!cfg_get_bool (p->host, p->alias, "show", 0))
166 return bad_request_error (p,
167 "you are not permitted to view files "
168 "in this directory");
170 /* Map the file's name to its MIME type. */
171 if ((extv = prematch (p->pool, p->remainder, re_ext, 0)) != 0)
175 vector_get (extv, 1, ext);
176 mime_type = mime_types_get_type (ext);
178 if (!mime_type) mime_type = "application/octet-stream"; /* Default. */
180 /* Check the hash to see if we know anything about this file already. */
181 memset (&key, 0, sizeof key);
182 key.st_dev = p->statbuf.st_dev;
183 key.st_ino = p->statbuf.st_ino;
184 if (hash_get (file_hash, key, offset))
187 vector_get (file_list, offset, info);
189 /* ... but has the file on disk changed since we mapped it? */
190 if (info.statbuf.st_mtime == p->statbuf.st_mtime)
191 return quickly_serve_it (p, &info, mime_type);
193 /* File has changed: invalidate the cache entry. */
194 delete_pool (info.pool);
197 /* Try to open the file. */
198 fd = open (p->file_path, O_RDONLY);
199 if (fd < 0) return file_not_found_error (p);
201 /* Set the FD_CLOEXEC flag so that when we fork off CGI scripts, they
202 * won't inherit the file descriptor.
204 if (fcntl (fd, F_SETFD, FD_CLOEXEC) < 0) { perror ("fcntl"); exit (1); }
206 /* If the file's too large, don't mmap it. */
207 if (p->statbuf.st_size > MAX_MMAP_SIZE)
208 return slowly_serve_it (p, fd, mime_type);
210 /* Map the file into memory. */
211 m = mmap (0, p->statbuf.st_size, PROT_READ, MAP_SHARED, fd, 0);
213 return slowly_serve_it (p, fd, mime_type);
217 /* Evict some entries from the cache to make enough room. */
218 while (nr_entries >= MAX_ENTRIES || total_size >= MAX_SIZE)
220 vector_get (lru_list, 0, offset);
221 vector_get (file_list, offset, info);
222 delete_pool (info.pool);
225 /* Add the entry to the cache. */
226 info.pool = new_subpool (file_pool);
227 info.statbuf = p->statbuf;
230 total_size += p->statbuf.st_size;
232 for (offset = 0; offset < vector_size (file_list); ++offset)
234 struct file_info entry;
236 vector_get (file_list, offset, entry);
239 vector_replace (file_list, offset, info);
244 vector_push_back (file_list, info);
247 hash_insert (file_hash, key, offset);
248 vector_push_back (lru_list, offset);
250 pool_register_cleanup_fn (info.pool, invalidate_entry, (void *) offset);
252 /* Serve it from memory. */
253 return quickly_serve_it (p, &info, mime_type);
257 quickly_serve_it (process_rq p, const struct file_info *info,
258 const char *mime_type)
260 http_response http_response;
263 /* Not changed, so it's a real cache hit. */
264 http_response = new_http_response (p->pool, p->http_request, p->io,
266 http_response_send_headers (http_response,
268 "Content-Type", mime_type,
269 /* Content length. */
270 "Content-Length", pitoa (p->pool,
271 info->statbuf.st_size),
272 /* End of headers. */
274 expires_header (p, http_response);
275 cl = http_response_end_headers (http_response);
277 if (http_request_is_HEAD (p->http_request)) return cl;
279 io_fwrite (info->addr, info->statbuf.st_size, 1, p->io);
285 slowly_serve_it (process_rq p, int fd, const char *mime_type)
287 http_response http_response;
289 char *buffer = alloca (n);
292 /* Cannot memory map this file. Instead fall back to just reading
293 * it and sending it back through the socket.
295 http_response = new_http_response (p->pool, p->http_request, p->io,
297 http_response_send_headers (http_response,
299 "Content-Type", mime_type,
300 /* Content length. */
301 "Content-Length", pitoa (p->pool,
303 /* End of headers. */
305 expires_header (p, http_response);
306 cl = http_response_end_headers (http_response);
308 if (http_request_is_HEAD (p->http_request)) return cl;
310 while ((r = read (fd, buffer, n)) > 0)
312 io_fwrite (buffer, r, 1, p->io);
325 /* Send the Expires header, if configured. */
327 expires_header (process_rq p, http_response http_response)
333 expires = cfg_get_string (p->host, p->alias, "expires", 0);
334 if (!expires) return;
336 /* Parse the configuration string. */
337 if (sscanf (expires, "%c%d%c", &pm, &length, &unit) == 3 &&
338 (pm == '+' || pm == '-') &&
340 (unit == 's' || unit == 'm' || unit == 'h' ||
341 unit == 'd' || unit == 'y'))
353 case 's': t += length; break;
354 case 'm': t += length * 60; break;
355 case 'h': t += length * (60 * 60); break;
356 case 'd': t += length * (60 * 60 * 24); break;
357 case 'y': t += length * (60 * 60 * 24 * 366); break;
364 case 's': t -= length; break;
365 case 'm': t -= length * 60; break;
366 case 'h': t -= length * (60 * 60); break;
367 case 'd': t -= length * (60 * 60 * 24); break;
368 case 'y': t -= length * (60 * 60 * 24 * 366); break;
373 strftime (header, sizeof header, "%a, %d %b %Y %H:%M:%S GMT", tm);
375 http_response_send_header (http_response, "Expires", header);
379 fprintf (stderr, "file.c: expires_header: cannot parse '%s'\n",
385 invalidate_entry (void *offset_ptr)
387 int offset = (int) offset_ptr;
389 struct file_info info;
392 /* Pull the invalidated entry out of the file_list. */
393 vector_get (file_list, offset, info);
395 /* Remove from the file_hash. */
396 memset (&key, 0, sizeof key);
397 key.st_dev = info.statbuf.st_dev;
398 key.st_ino = info.statbuf.st_ino;
399 if (!hash_erase (file_hash, key)) abort ();
401 /* Remove from the lru_list. */
402 for (i = 0; i < vector_size (lru_list); ++i)
404 vector_get (lru_list, i, j);
408 vector_erase (lru_list, i);
415 /* Unmap the memory. */
416 munmap (info.addr, info.statbuf.st_size);
418 /* Invalidate this entry in the file_list. */
420 vector_replace (file_list, offset, info);
422 /* Update counters. */
424 total_size -= info.statbuf.st_size;