Add to git.
[rws.git] / file.c
1 /* File serving.
2  * - by Richard W.M. Jones <rich@annexia.org>
3  *
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.
8  *
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.
13  *
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.
17  *
18  * $Id: file.c,v 1.15 2003/02/05 23:02:51 rich Exp $
19  */
20
21 #include "config.h"
22
23 #include <stdio.h>
24 #include <stdlib.h>
25
26 #ifdef HAVE_UNISTD_H
27 #include <unistd.h>
28 #endif
29
30 #ifdef HAVE_STRING_H
31 #include <string.h>
32 #endif
33
34 #ifdef HAVE_SYS_TYPES_H
35 #include <sys/types.h>
36 #endif
37
38 #ifdef HAVE_SYS_STAT_H
39 #include <sys/stat.h>
40 #endif
41
42 #ifdef HAVE_SYS_MMAN_H
43 #include <sys/mman.h>
44 #endif
45
46 #ifdef HAVE_FCNTL_H
47 #include <fcntl.h>
48 #endif
49
50 #ifdef HAVE_TIME_H
51 #include <time.h>
52 #endif
53
54 #ifdef HAVE_ALLOCA_H
55 #include <alloca.h>
56 #endif
57
58 #include <pool.h>
59 #include <vector.h>
60 #include <hash.h>
61 #include <pstring.h>
62 #include <pre.h>
63
64 #include <pthr_pseudothread.h>
65 #include <pthr_http.h>
66 #include <pthr_iolib.h>
67
68 #include "process_rq.h"
69 #include "mime_types.h"
70 #include "errors.h"
71 #include "exec.h"
72 #include "exec_so.h"
73 #include "cfg.h"
74 #include "re.h"
75 #include "file.h"
76
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.
79  *
80  * Similarly the code should send "Last-Modified" headers.
81  */
82
83 struct hash_key
84 {
85   dev_t st_dev;
86   ino_t st_ino;
87 };
88
89 struct file_info
90 {
91   struct pool *pool;
92   struct stat statbuf;
93   void *addr;
94 };
95
96 static pool file_pool = 0;
97
98 #define MAX_MMAP_SIZE (10 * 1024 * 1024)
99 #define MAX_ENTRIES   100
100 #define MAX_SIZE      (100 * 1024 * 1024)
101
102 static int total_size = 0;
103 static int nr_entries = 0;
104
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).
108  */
109 static vector file_list = 0;
110
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.
114  */
115 static vector lru_list = 0;
116
117 /* This hash of { device, inode } -> integer maps unique stat information
118  * about files to their offset in the file_list array above.
119  */
120 static hash file_hash = 0;
121
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);
126
127 /* Initialize structures. */
128 void
129 file_init ()
130 {
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);
135 }
136
137 int
138 file_serve (process_rq p)
139 {
140   vector extv;
141   const char *mime_type = 0;
142   int offset, fd;
143   struct hash_key key;
144   struct file_info info;
145   void *m;
146
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.
150    */
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);
155
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
158    * run.
159    */
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);
163
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");
169
170   /* Map the file's name to its MIME type. */
171   if ((extv = prematch (p->pool, p->remainder, re_ext, 0)) != 0)
172     {
173       char *ext;
174
175       vector_get (extv, 1, ext);
176       mime_type = mime_types_get_type (ext);
177     }
178   if (!mime_type) mime_type = "application/octet-stream"; /* Default. */
179
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))
185     {
186       /* Cache hit ... */
187       vector_get (file_list, offset, info);
188
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);
192       else
193         /* File has changed: invalidate the cache entry. */
194         delete_pool (info.pool);
195     }
196
197   /* Try to open the file. */
198   fd = open (p->file_path, O_RDONLY);
199   if (fd < 0) return file_not_found_error (p);
200
201   /* Set the FD_CLOEXEC flag so that when we fork off CGI scripts, they
202    * won't inherit the file descriptor.
203    */
204   if (fcntl (fd, F_SETFD, FD_CLOEXEC) < 0) { perror ("fcntl"); exit (1); }
205
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);
209
210   /* Map the file into memory. */
211   m = mmap (0, p->statbuf.st_size, PROT_READ, MAP_SHARED, fd, 0);
212   if (m == MAP_FAILED)
213     return slowly_serve_it (p, fd, mime_type);
214
215   close (fd);
216
217   /* Evict some entries from the cache to make enough room. */
218   while (nr_entries >= MAX_ENTRIES || total_size >= MAX_SIZE)
219     {
220       vector_get (lru_list, 0, offset);
221       vector_get (file_list, offset, info);
222       delete_pool (info.pool);
223     }
224
225   /* Add the entry to the cache. */
226   info.pool = new_subpool (file_pool);
227   info.statbuf = p->statbuf;
228   info.addr = m;
229   nr_entries++;
230   total_size += p->statbuf.st_size;
231
232   for (offset = 0; offset < vector_size (file_list); ++offset)
233     {
234       struct file_info entry;
235
236       vector_get (file_list, offset, entry);
237       if (entry.pool == 0)
238         {
239           vector_replace (file_list, offset, info);
240           goto added_it;
241         }
242     }
243
244   vector_push_back (file_list, info);
245
246  added_it:
247   hash_insert (file_hash, key, offset);
248   vector_push_back (lru_list, offset);
249
250   pool_register_cleanup_fn (info.pool, invalidate_entry, (void *) offset);
251
252   /* Serve it from memory. */
253   return quickly_serve_it (p, &info, mime_type);
254 }
255
256 static int
257 quickly_serve_it (process_rq p, const struct file_info *info,
258                   const char *mime_type)
259 {
260   http_response http_response;
261   int cl;
262
263   /* Not changed, so it's a real cache hit. */
264   http_response = new_http_response (p->pool, p->http_request, p->io,
265                                      200, "OK");
266   http_response_send_headers (http_response,
267                               /* Content type. */
268                               "Content-Type", mime_type,
269                               /* Content length. */
270                               "Content-Length", pitoa (p->pool,
271                                                        info->statbuf.st_size),
272                               /* End of headers. */
273                               NULL);
274   expires_header (p, http_response);
275   cl = http_response_end_headers (http_response);
276
277   if (http_request_is_HEAD (p->http_request)) return cl;
278
279   io_fwrite (info->addr, info->statbuf.st_size, 1, p->io);
280
281   return cl;
282 }
283
284 static int
285 slowly_serve_it (process_rq p, int fd, const char *mime_type)
286 {
287   http_response http_response;
288   const int n = 4096;
289   char *buffer = alloca (n);
290   int r, cl;
291
292   /* Cannot memory map this file. Instead fall back to just reading
293    * it and sending it back through the socket.
294    */
295   http_response = new_http_response (p->pool, p->http_request, p->io,
296                                      200, "OK");
297   http_response_send_headers (http_response,
298                               /* Content type. */
299                               "Content-Type", mime_type,
300                               /* Content length. */
301                               "Content-Length", pitoa (p->pool,
302                                                        p->statbuf.st_size),
303                               /* End of headers. */
304                               NULL);
305   expires_header (p, http_response);
306   cl = http_response_end_headers (http_response);
307
308   if (http_request_is_HEAD (p->http_request)) return cl;
309
310   while ((r = read (fd, buffer, n)) > 0)
311     {
312       io_fwrite (buffer, r, 1, p->io);
313     }
314
315   if (r < 0)
316     {
317       perror ("read");
318     }
319
320   close (fd);
321
322   return cl;
323 }
324
325 /* Send the Expires header, if configured. */
326 static void
327 expires_header (process_rq p, http_response http_response)
328 {
329   const char *expires;
330   char pm, unit;
331   int length;
332
333   expires = cfg_get_string (p->host, p->alias, "expires", 0);
334   if (!expires) return;
335
336   /* Parse the configuration string. */
337   if (sscanf (expires, "%c%d%c", &pm, &length, &unit) == 3 &&
338       (pm == '+' || pm == '-') &&
339       length > 0 &&
340       (unit == 's' || unit == 'm' || unit == 'h' ||
341        unit == 'd' || unit == 'y'))
342     {
343       time_t t;
344       struct tm *tm;
345       char header[64];
346
347       time (&t);
348
349       if (pm == '+')
350         {
351           switch (unit)
352             {
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;
358             }
359         }
360       else
361         {
362           switch (unit)
363             {
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;
369             }
370         }
371
372       tm = gmtime (&t);
373       strftime (header, sizeof header, "%a, %d %b %Y %H:%M:%S GMT", tm);
374
375       http_response_send_header (http_response, "Expires", header);
376     }
377   else
378     {
379       fprintf (stderr, "file.c: expires_header: cannot parse '%s'\n",
380                expires);
381     }
382 }
383
384 static void
385 invalidate_entry (void *offset_ptr)
386 {
387   int offset = (int) offset_ptr;
388   int i, j;
389   struct file_info info;
390   struct hash_key key;
391
392   /* Pull the invalidated entry out of the file_list. */
393   vector_get (file_list, offset, info);
394
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 ();
400
401   /* Remove from the lru_list. */
402   for (i = 0; i < vector_size (lru_list); ++i)
403     {
404       vector_get (lru_list, i, j);
405
406       if (j == offset)
407         {
408           vector_erase (lru_list, i);
409           goto found_it;
410         }
411     }
412   abort ();
413
414  found_it:
415   /* Unmap the memory. */
416   munmap (info.addr, info.statbuf.st_size);
417
418   /* Invalidate this entry in the file_list. */
419   info.pool = 0;
420   vector_replace (file_list, offset, info);
421
422   /* Update counters. */
423   nr_entries--;
424   total_size -= info.statbuf.st_size;
425 }