dirplex: Ignore backup files.
[ashd.git] / src / dirplex.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 <stdio.h>
21 #include <string.h>
22 #include <unistd.h>
23 #include <errno.h>
24 #include <sys/stat.h>
25 #include <ctype.h>
26 #include <dirent.h>
27 #include <fnmatch.h>
28 #include <time.h>
29
30 #ifdef HAVE_CONFIG_H
31 #include <config.h>
32 #endif
33 #include <utils.h>
34 #include <mt.h>
35 #include <log.h>
36 #include <req.h>
37 #include <proc.h>
38 #include <resp.h>
39 #include <cf.h>
40
41 #define PAT_BASENAME 0
42 #define PAT_PATHNAME 1
43 #define PAT_ALL 2
44 #define PAT_DEFAULT 3
45
46 #define PT_FILE 0
47 #define PT_DIR 1
48
49 struct config {
50     struct config *next, *prev;
51     char *path;
52     time_t mtime, lastck;
53     struct child *children;
54     struct pattern *patterns;
55     char **index;
56 };
57
58 struct rule {
59     int type;
60     char **patterns;
61 };
62
63 struct pattern {
64     struct pattern *next;
65     int type;
66     char *childnm;
67     char **fchild;
68     struct rule **rules;
69 };
70
71 static struct config *cflist;
72 static struct config *gconfig, *lconfig;
73 static time_t now;
74
75 static void freerule(struct rule *rule)
76 {
77     freeca(rule->patterns);
78     free(rule);
79 }
80
81 static void freepattern(struct pattern *pat)
82 {
83     struct rule **rule;
84     
85     for(rule = pat->rules; *rule; rule++)
86         freerule(*rule);
87     if(pat->childnm != NULL)
88         free(pat->childnm);
89     freeca(pat->fchild);
90     free(pat);
91 }
92
93 static void freeconfig(struct config *cf)
94 {
95     struct child *ch, *nch;
96     struct pattern *pat, *npat;
97     
98     if(cf->prev != NULL)
99         cf->prev->next = cf->next;
100     if(cf->next != NULL)
101         cf->next->prev = cf->prev;
102     if(cf == cflist)
103         cflist = cf->next;
104     if(cf->path != NULL)
105         free(cf->path);
106     for(ch = cf->children; ch != NULL; ch = nch) {
107         nch = ch->next;
108         freechild(ch);
109     }
110     for(pat = cf->patterns; pat != NULL; pat = npat) {
111         npat = pat->next;
112         freepattern(pat);
113     }
114     freeca(cf->index);
115     free(cf);
116 }
117
118 static struct child *getchild(struct config *cf, char *name)
119 {
120     struct child *ch;
121     
122     for(ch = cf->children; ch; ch = ch->next) {
123         if(!strcmp(ch->name, name))
124             break;
125     }
126     return(ch);
127 }
128
129 static struct rule *newrule(struct pattern *pat)
130 {
131     int i;
132     struct rule *rule;
133     
134     for(i = 0; pat->rules[i]; i++);
135     pat->rules = srealloc(pat->rules, sizeof(*pat->rules) * (i + 2));
136     rule = pat->rules[i] = szmalloc(sizeof(*rule));
137     pat->rules[i + 1] = NULL;
138     return(rule);
139 }
140
141 static struct pattern *newpattern(void)
142 {
143     struct pattern *pat;
144     
145     omalloc(pat);
146     pat->rules = szmalloc(sizeof(*pat->rules));
147     return(pat);
148 }
149
150 static char **cadup(char **w)
151 {
152     char **ret;
153     int i, l;
154     
155     l = calen(w);
156     ret = smalloc(sizeof(*ret) * (l + 1));
157     for(i = 0; i < l; i++)
158         ret[i] = sstrdup(w[i]);
159     ret[i] = NULL;
160     return(ret);
161 }
162
163 static struct pattern *parsepattern(struct cfstate *s)
164 {
165     struct pattern *pat;
166     struct rule *rule;
167     int sl;
168
169     if(!strcmp(s->argv[0], "match")) {
170         s->expstart = 1;
171         pat = newpattern();
172     } else {
173         return(NULL);
174     }
175     
176     if((s->argc > 1) && !strcmp(s->argv[1], "directory"))
177         pat->type = PT_DIR;
178     sl = s->lno;
179     while(1) {
180         getcfline(s);
181         if(!strcmp(s->argv[0], "filename")) {
182             if(s->argc < 2) {
183                 flog(LOG_WARNING, "%s:%i: missing pattern for `filename' match", s->file, s->lno);
184                 continue;
185             }
186             rule = newrule(pat);
187             rule->type = PAT_BASENAME;
188             rule->patterns = cadup(s->argv + 1);
189         } else if(!strcmp(s->argv[0], "pathname")) {
190             if(s->argc < 2) {
191                 flog(LOG_WARNING, "%s:%i: missing pattern for `pathname' match", s->file, s->lno);
192                 continue;
193             }
194             rule = newrule(pat);
195             rule->type = PAT_PATHNAME;
196             rule->patterns = cadup(s->argv + 1);
197         } else if(!strcmp(s->argv[0], "all")) {
198             newrule(pat)->type = PAT_ALL;
199         } else if(!strcmp(s->argv[0], "default")) {
200             newrule(pat)->type = PAT_DEFAULT;
201         } else if(!strcmp(s->argv[0], "handler")) {
202             if(s->argc < 2) {
203                 flog(LOG_WARNING, "%s:%i: missing child name for `handler' directive", s->file, s->lno);
204                 continue;
205             }
206             if(pat->childnm != NULL)
207                 free(pat->childnm);
208             pat->childnm = sstrdup(s->argv[1]);
209         } else if(!strcmp(s->argv[0], "fork")) {
210             pat->fchild = cadup(s->argv + 1);
211         } else if(!strcmp(s->argv[0], "end") || !strcmp(s->argv[0], "eof")) {
212             break;
213         } else {
214             flog(LOG_WARNING, "%s:%i: unknown directive `%s' in pattern declaration", s->file, s->lno, s->argv[0]);
215         }
216     }
217     
218     if(pat->rules[0] == NULL) {
219         flog(LOG_WARNING, "%s:%i: missing rules in match declaration", s->file, sl);
220         freepattern(pat);
221         return(NULL);
222     }
223     if((pat->childnm == NULL) && (pat->fchild == NULL)) {
224         flog(LOG_WARNING, "%s:%i: missing handler in match declaration", s->file, sl);
225         freepattern(pat);
226         return(NULL);
227     }
228     return(pat);
229 }
230
231 static struct config *emptyconfig(void)
232 {
233     struct config *cf;
234     
235     omalloc(cf);
236     return(cf);
237 }
238
239 static struct config *readconfig(char *file)
240 {
241     struct cfstate *s;
242     FILE *in;
243     struct config *cf;
244     struct child *child;
245     struct pattern *pat;
246     
247     if((in = fopen(file, "r")) == NULL) {
248         flog(LOG_WARNING, "%s: %s", file, strerror(errno));
249         return(NULL);
250     }
251     s = mkcfparser(in, file);
252     cf = emptyconfig();
253     
254     while(1) {
255         getcfline(s);
256         if((child = parsechild(s)) != NULL) {
257             child->next = cf->children;
258             cf->children = child;
259         } else if((pat = parsepattern(s)) != NULL) {
260             pat->next = cf->patterns;
261             cf->patterns = pat;
262         } else if(!strcmp(s->argv[0], "index-file")) {
263             freeca(cf->index);
264             cf->index = NULL;
265             if(s->argc > 1)
266                 cf->index = cadup(s->argv + 1);
267         } else if(!strcmp(s->argv[0], "eof")) {
268             break;
269         } else {
270             flog(LOG_WARNING, "%s:%i: unknown directive `%s'", s->file, s->lno, s->argv[0]);
271         }
272     }
273     
274     freecfparser(s);
275     fclose(in);
276     return(cf);
277 }
278
279 static struct config *getconfig(char *path)
280 {
281     struct config *cf;
282     struct stat sb;
283     char *fn;
284     time_t mtime;
285     
286     fn = sprintf3("%s/.htrc", path);
287     for(cf = cflist; cf != NULL; cf = cf->next) {
288         if(!strcmp(cf->path, path)) {
289             if(now - cf->lastck > 5) {
290                 if(stat(fn, &sb) || (sb.st_mtime != cf->mtime)) {
291                     freeconfig(cf);
292                     break;
293                 }
294             }
295             cf->lastck = now;
296             return(cf);
297         }
298     }
299     if(access(fn, R_OK) || stat(fn, &sb)) {
300         cf = emptyconfig();
301         mtime = 0;
302     } else {
303         if((cf = readconfig(fn)) == NULL)
304             return(NULL);
305         mtime = sb.st_mtime;
306     }
307     cf->path = sstrdup(path);
308     cf->mtime = mtime;
309     cf->lastck = now;
310     cf->next = cflist;
311     cf->prev = NULL;
312     if(cflist != NULL)
313         cflist->prev = cf;
314     cflist = cf;
315     return(cf);
316 }
317
318 static struct config **getconfigs(char *file)
319 {
320     static struct config **ret = NULL;
321     struct {
322         struct config **b;
323         size_t s, d;
324     } buf;
325     struct config *cf;
326     char *tmp, *p;
327     
328     if(ret != NULL)
329         free(ret);
330     bufinit(buf);
331     tmp = sstrdup(file);
332     while(1) {
333         if((p = strrchr(tmp, '/')) == NULL)
334             break;
335         *p = 0;
336         if((cf = getconfig(tmp)) != NULL)
337             bufadd(buf, cf);
338     }
339     free(tmp);
340     if((cf = getconfig(".")) != NULL)
341         bufadd(buf, cf);
342     if(lconfig != NULL)
343         bufadd(buf, lconfig);
344     if(gconfig != NULL)
345         bufadd(buf, gconfig);
346     bufadd(buf, NULL);
347     return(ret = buf.b);
348 }
349
350 static struct child *findchild(char *file, char *name)
351 {
352     int i;
353     struct config **cfs;
354     struct child *ch;
355     
356     cfs = getconfigs(file);
357     for(i = 0; cfs[i] != NULL; i++) {
358         if((ch = getchild(cfs[i], name)) != NULL)
359             break;
360     }
361     return(ch);
362 }
363
364 static struct pattern *findmatch(char *file, int trydefault, int dir)
365 {
366     int i, o, c;
367     char *bn;
368     struct config **cfs;
369     struct pattern *pat;
370     struct rule *rule;
371     
372     if((bn = strrchr(file, '/')) != NULL)
373         bn++;
374     else
375         bn = file;
376     cfs = getconfigs(file);
377     for(c = 0; cfs[c] != NULL; c++) {
378         for(pat = cfs[c]->patterns; pat != NULL; pat = pat->next) {
379             if(!dir && (pat->type == PT_DIR))
380                 continue;
381             if(dir && (pat->type != PT_DIR))
382                 continue;
383             for(i = 0; (rule = pat->rules[i]) != NULL; i++) {
384                 if(rule->type == PAT_BASENAME) {
385                     for(o = 0; rule->patterns[o] != NULL; o++) {
386                         if(!fnmatch(rule->patterns[o], bn, 0))
387                             break;
388                     }
389                     if(rule->patterns[o] == NULL)
390                         break;
391                 } else if(rule->type == PAT_PATHNAME) {
392                     for(o = 0; rule->patterns[o] != NULL; o++) {
393                         if(!fnmatch(rule->patterns[o], file, FNM_PATHNAME))
394                             break;
395                     }
396                     if(rule->patterns[o] == NULL)
397                         break;
398                 } else if(rule->type == PAT_ALL) {
399                 } else if(rule->type == PAT_DEFAULT) {
400                     if(!trydefault)
401                         break;
402                 }
403             }
404             if(!rule)
405                 return(pat);
406         }
407     }
408     if(!trydefault)
409         return(findmatch(file, 1, dir));
410     return(NULL);
411 }
412
413 static void handle(struct hthead *req, int fd, char *path, struct pattern *pat)
414 {
415     struct child *ch;
416
417     headappheader(req, "X-Ash-File", path);
418     if(pat->fchild) {
419         stdforkserve(pat->fchild, req, fd);
420     } else {
421         if((ch = findchild(path, pat->childnm)) == NULL) {
422             flog(LOG_ERR, "child %s requested, but was not declared", pat->childnm);
423             simpleerror(fd, 500, "Configuration Error", "The server is erroneously configured. Handler %s was requested, but not declared.", pat->childnm);
424             return;
425         }
426         if(childhandle(ch, req, fd))
427             simpleerror(fd, 500, "Server Error", "The request handler crashed.");
428     }
429 }
430
431 static void handlefile(struct hthead *req, int fd, char *path)
432 {
433     struct pattern *pat;
434
435     if((pat = findmatch(path, 0, 0)) == NULL) {
436         simpleerror(fd, 404, "Not Found", "The requested URL has no corresponding resource.");
437         return;
438     }
439     handle(req, fd, path, pat);
440 }
441
442 static void handledir(struct hthead *req, int fd, char *path)
443 {
444     struct config **cfs;
445     int i, o;
446     struct stat sb;
447     char *inm, *ipath, *p, *cpath;
448     DIR *dir;
449     struct dirent *dent;
450     struct pattern *pat;
451     
452     cpath = sprintf2("%s/", path);
453     cfs = getconfigs(cpath);
454     for(i = 0; cfs[i] != NULL; i++) {
455         if(cfs[i]->index != NULL) {
456             for(o = 0; cfs[i]->index[o] != NULL; o++) {
457                 inm = cfs[i]->index[o];
458                 ipath = sprintf2("%s/%s", path, inm);
459                 if(!stat(ipath, &sb) && S_ISREG(sb.st_mode)) {
460                     handlefile(req, fd, ipath);
461                     free(ipath);
462                     goto out;
463                 }
464                 free(ipath);
465                 
466                 ipath = NULL;
467                 if(!strchr(inm, '.') && ((dir = opendir(path)) != NULL)) {
468                     while((dent = readdir(dir)) != NULL) {
469                         /* Ignore backup files.
470                          * XXX: There is probably a better and more
471                          * extensible way to do this. */
472                         if(dent->d_name[strlen(dent->d_name) - 1] == '~')
473                             continue;
474                         if((p = strchr(dent->d_name, '.')) == NULL)
475                             continue;
476                         if(strncmp(dent->d_name, inm, strlen(inm)))
477                             continue;
478                         ipath = sprintf2("%s/%s", path, dent->d_name);
479                         if(stat(ipath, &sb) || !S_ISREG(sb.st_mode)) {
480                             free(ipath);
481                             ipath = NULL;
482                             continue;
483                         }
484                         break;
485                     }
486                     closedir(dir);
487                 }
488                 if(ipath != NULL) {
489                     handlefile(req, fd, ipath);
490                     free(ipath);
491                     goto out;
492                 }
493             }
494             break;
495         }
496     }
497     if((pat = findmatch(cpath, 0, 1)) != NULL) {
498         handle(req, fd, cpath, pat);
499         goto out;
500     }
501     simpleerror(fd, 403, "Not Authorized", "Will not send listings for this directory.");
502     
503 out:
504     free(cpath);
505 }
506
507 static int checkdir(struct hthead *req, int fd, char *path)
508 {
509     return(0);
510 }
511
512 static void serve(struct hthead *req, int fd)
513 {
514     char *p, *p2, *path, *tmp, *buf, *p3, *nm;
515     struct stat sb;
516     DIR *dir;
517     struct dirent *dent;
518     
519     now = time(NULL);
520     nm = req->rest;
521     path = sstrdup(".");
522     p = nm;
523     while(1) {
524         if((p2 = strchr(p, '/')) == NULL) {
525         } else {
526             *(p2++) = 0;
527         }
528         if((tmp = unquoteurl(p)) == NULL) {
529             simpleerror(fd, 400, "Bad Request", "The requested URL contains an invalid escape sequence.");
530             goto fail;
531         }
532         strcpy(p, tmp);
533         free(tmp);
534         
535         if(!*p) {
536             if(p2 == NULL) {
537                 if(stat(path, &sb)) {
538                     flog(LOG_WARNING, "failed to stat previously stated directory %s: %s", path, strerror(errno));
539                     simpleerror(fd, 500, "Internal Server Error", "The server encountered an unexpected condition.");
540                     goto fail;
541                 }
542                 break;
543             } else {
544                 simpleerror(fd, 404, "Not Found", "The requested URL has no corresponding resource.");
545                 goto fail;
546             }
547         }
548         if(*p == '.') {
549             simpleerror(fd, 404, "Not Found", "The requested URL has no corresponding resource.");
550             goto fail;
551         }
552         
553         getconfig(path);
554         
555         /*
556          * First, check the name verbatimely:
557          */
558         buf = sprintf3("%s/%s", path, p);
559         if(!stat(buf, &sb)) {
560             if(S_ISDIR(sb.st_mode)) {
561                 tmp = path;
562                 if(!strcmp(path, "."))
563                     path = sstrdup(p);
564                 else
565                     path = sprintf2("%s/%s", path, p);
566                 free(tmp);
567                 if(p2 == NULL) {
568                     stdredir(req, fd, 301, sprintf3("%s/", p));
569                     goto out;
570                 }
571                 if(checkdir(req, fd, path))
572                     break;
573                 goto next;
574             }
575             if(S_ISREG(sb.st_mode)) {
576                 tmp = path;
577                 path = sprintf2("%s/%s", path, p);
578                 free(tmp);
579                 break;
580             }
581             simpleerror(fd, 404, "Not Found", "The requested URL has no corresponding resource.");
582             goto fail;
583         }
584
585         /*
586          * Check the file extensionlessly:
587          */
588         if(!strchr(p, '.') && ((dir = opendir(path)) != NULL)) {
589             while((dent = readdir(dir)) != NULL) {
590                 buf = sprintf3("%s/%s", path, dent->d_name);
591                 /* Ignore backup files.
592                  * XXX: There is probably a better and more
593                  * extensible way to do this. */
594                 if(dent->d_name[strlen(dent->d_name) - 1] == '~')
595                     continue;
596                 if((p3 = strchr(dent->d_name, '.')) != NULL)
597                     *p3 = 0;
598                 if(strcmp(dent->d_name, p))
599                     continue;
600                 if(stat(buf, &sb))
601                     continue;
602                 if(!S_ISREG(sb.st_mode))
603                     continue;
604                 tmp = path;
605                 path = sstrdup(buf);
606                 free(tmp);
607                 break;
608             }
609             closedir(dir);
610             if(dent != NULL)
611                 break;
612         }
613         
614         simpleerror(fd, 404, "Not Found", "The requested URL has no corresponding resource.");
615         goto fail;
616         
617     next:
618         if(p2 == NULL)
619             break;
620         p = p2;
621     }
622     if(p2 == NULL)
623         replrest(req, "");
624     else
625         replrest(req, p2);
626     if(!strncmp(path, "./", 2))
627         memmove(path, path + 2, strlen(path + 2) + 1);
628     if(S_ISDIR(sb.st_mode)) {
629         handledir(req, fd, path);
630     } else if(S_ISREG(sb.st_mode)) {
631         handlefile(req, fd, path);
632     } else {
633         simpleerror(fd, 404, "Not Found", "The requested URL has no corresponding resource.");
634         goto fail;
635     }
636     goto out;
637     
638 fail:
639     /* No special handling, for now at least. */
640 out:
641     free(path);
642 }
643
644 static void usage(FILE *out)
645 {
646     fprintf(out, "usage: dirplex [-hN] [-c CONFIG] DIR\n");
647 }
648
649 int main(int argc, char **argv)
650 {
651     int c;
652     int nodef;
653     char *gcf, *lcf, *clcf;
654     struct hthead *req;
655     int fd;
656     
657     nodef = 0;
658     lcf = NULL;
659     while((c = getopt(argc, argv, "hNc:")) >= 0) {
660         switch(c) {
661         case 'h':
662             usage(stdout);
663             exit(0);
664         case 'N':
665             nodef = 1;
666             break;
667         case 'c':
668             lcf = optarg;
669             break;
670         default:
671             usage(stderr);
672             exit(1);
673         }
674     }
675     if(argc - optind < 1) {
676         usage(stderr);
677         exit(1);
678     }
679     if(!nodef) {
680         if((gcf = findstdconf("ashd/dirplex.rc")) != NULL) {
681             gconfig = readconfig(gcf);
682             free(gcf);
683         }
684     }
685     if(lcf != NULL) {
686         if(strchr(lcf, '/') == NULL) {
687             if((clcf = findstdconf(sprintf3("ashd/%s", lcf))) == NULL) {
688                 flog(LOG_ERR, "could not find requested configuration `%s'", lcf);
689                 exit(1);
690             }
691             if((lconfig = readconfig(clcf)) == NULL)
692                 exit(1);
693             free(clcf);
694         } else {
695             if((lconfig = readconfig(lcf)) == NULL)
696                 exit(1);
697         }
698     }
699     if(chdir(argv[optind])) {
700         flog(LOG_ERR, "could not change directory to %s: %s", argv[optind], strerror(errno));
701         exit(1);
702     }
703     signal(SIGCHLD, SIG_IGN);
704     while(1) {
705         if((fd = recvreq(0, &req)) < 0) {
706             if(errno != 0)
707                 flog(LOG_ERR, "recvreq: %s", strerror(errno));
708             break;
709         }
710         serve(req, fd);
711         freehthead(req);
712         close(fd);
713     }
714     return(0);
715 }