/* * Web discussion groups. * The group is identified by an argument. * Each group is kept as a flat directory. * There we keep a file per article. * Each file is made of a series of sections. * The first section is considered the article * The following ones are the replies. * The Id determines to which post they are replying. * Number | Id.Number * title * Author * body lines... * \a (ascii 07 / bell) */ #include #include #include #include #include "httpd.h" #include "httpsrv.h" #include "article.h" enum { Nfront = 10, // max # in front page }; typedef struct Req Req; struct Req{ char* group; char* file; int op; HSPairs*args; }; typedef struct Opf Opf; struct Opf { char op; void (*f)(Req*); }; static Hio* hout; static Hio houtb; static int vermaj; static HConnect*hcon; static int debug = 0; static char Dfltreq[]= "o=i&g=dso"; static char Top[] = "/groups"; static char Mark[] = "DYNAMIC"; /* * Strings seen in output, to help with localization. */ static char Msearchresults[] = "Resultados de la búsqueda"; static char Mreturn[] = "Volver."; static char Msendnew[]= "Enviar nuevo articulo"; static char Mreply[] = "Responder"; static char Mreplyto[]= "Responder a..."; static char Manswers[] = "resp."; static char Mgoonreading[] = "Seguir leyendo otros"; static char Mgoback[] = "Volver atras"; static char Mreadmore[] = "Leer mas"; static char Mreturntoindex[] = "Volver a la portada"; static char Mwrote[] = "dice..."; static char Mby[] = "Por"; static char Mrequiredfieldmissing[] = "No has escrito en un campo requerido.\n" "Usa back en tu navegador e intenta de nuevo\n"; static char Mclickheretoreturn[] = "

Hecho. Pulsa " "aquí " "para volver.\n"; static char Mbodywithlonglines[]= "Tienes lineas muy largas. Usa back en tu navegador\n" "y usa return para que tu mensaje tenga lineas\n" "que se puedan leer bien (max 80 caracteres).\n"; static void badreq(void) { hfail(hcon, HBadReq); exits(nil); } static void notfound(char* o) { hfail(hcon, HNotFound, o); exits(nil); } static void failure(void) { hfail(hcon, HTempFail); exits(nil); } static void headers(char* title) { if(vermaj){ hokheaders(hcon); hprint(hout, "Content-type: text/html; charset=UTF-8\r\n"); hflush(hout); } if (title != nil) hprint(hout, "%s\n", title); } static void notice(char* fmt, ...) { va_list arg; char buf[1024], *out; va_start(arg, fmt); out = vseprint(buf, buf+sizeof(buf), fmt, arg); va_end(arg); *out = 0; headers("notice"); hprint(hout, "%s", buf); hprint(hout, "\n"); } static char* getarg(Req* r, char arg, int must) { HSPairs*p; for (p = r->args; p != nil; p = p->next){ if (p->s[0] == arg) return p->t; } if (must) badreq(); return nil; } static void parsereq(char* s, Req* r) { char* v; memset(r, 0, sizeof(Req)); r->args = hparsequery(hcon, s); v = getarg(r, 'o', 1); r->op = *v; v = getarg(r, 'g', 1); r->group = strdup(v); } static void outfilehdr(Biobuf* bin) { char* ln; while(ln = Brdstr(bin, '\n', 1)){ if (strcmp(ln, Mark) == 0){ free(ln); return; } hprint(hout, "%s\n", ln); free(ln); } } static void outfiletail(Biobuf* bin) { char* ln; while(ln = Brdstr(bin, '\n', 1)){ hprint(hout, "%s\n", ln); free(ln); } } static void getpostintro(Post* p, char* intro, int max, int* more) { char* e; char* w; int l; e = strecpy(intro, intro+max-4, p->body); l = e - intro; *more = (p->body[l] != 0); if (*more){ w = strrchr(intro, ' '); if (w) e = w; strcpy(e, "..."); } } static char* Mon[] = { "Ene", "Feb", "Mar", "Abr", "May", "Jun", "Jul", "Ago", "Sep", "Oct", "Nov", "Dic" }; static char* artdate(ulong mt) { Tm* tm; static char str[40]; if (mt == 0) return ""; tm = localtime(mt); seprint(str, str+40, "(%d-%s)", tm->mday, Mon[tm->mon]); return str; } static Biobuf* bopenfile(char* group, char* art, ulong* mt) { char* file; Biobuf* ib; Dir* d; file = smprint("%s/%s/%s", Top, group, art); if (strstr(file, "..")){ free(file); badreq(); } if (mt != nil){ *mt = 0; d = dirstat(file); if (d != nil){ *mt = d->mtime; free(d); } } ib = Bopen(file, OREAD); free(file); return ib; } static int outpostintro(char* g, char* file, int verbose) { Biobuf* bin; Post* p; int n; char intro[120]; int more; char* date; ulong mt; bin = bopenfile(g, file, &mt); if (bin == nil) return 0; if (debug) fprint(2, "%s/%s\n", g, file); p = readarticle(bin, &n); if (p == nil){ Bterm(bin); return 0; } date = artdate(mt); if(debug) fprint(2, "article %s\n", p->title); switch(verbose){ case 0: hprint(hout,"", g, file); hprint(hout, "%s (%s; %d posts)
\n", p->title, p->author, n); break; case 1: hprint(hout,"

", g, file); hprint(hout, "%s %s ", p->title, date); hprint(hout, "(%s %s)
\n", Mby, p->author); break; default: hprint(hout, "


%s %s", p->title, date); hprint(hout, "
%s %s
\n", Mby, p->author); getpostintro(p, intro, sizeof(intro), &more); hprint(hout, "%s
\n", intro); if (n > 1) hprint(hout, "Leer " "mas (%d %s.)

\n", g, file, n-1, Manswers); else hprint(hout, "%s" "

\n",g, file, Mreadmore); } closepost(p); Bterm(bin); return 1; } static char* readfile(char* fname) { Dir* d; vlong len; int fd; char* buf; fd = open(fname, OREAD); if (fd < 0) return nil; d = dirfstat(fd); assert (d != nil); len = d->length; free(d); buf = malloc(len +1); buf[0] = 0; readn(fd, buf, len); close(fd); buf[len] = 0; return buf; } static int idxsort(void* a, void* b) { Dir* da = a; Dir* db = b; if (da->name[0] != 'A') return 1; return (int)(da->mtime - db->mtime); } static char* readindex(Req* req, char*** entsp, int* nentsp) { Dir* d; char** ents; int fd; char* buf; char* dir; int nd; long l; int i, j; char* s; /* Experiment, read all article files, and * sort by mtime. */ if (strstr(req->group, "..")) notfound(".."); dir = smprint("%s/%s", Top, req->group); fd = open(dir, OREAD); if (fd < 0) failure(); nd = dirreadall(fd, &d); close(fd); if (nd <= 0){ *entsp = nil; *nentsp = 0; return nil; } qsort(d, nd, sizeof(d[0]), idxsort); l = 0; for (i = 0; i < nd; i++){ if (d[i].name[0] != 'A') continue; l += strlen(d[i].name) + 2; } s = buf = malloc(l); ents=malloc(sizeof(char*)*(nd+1)); if (buf == nil){ free(d); failure(); } for (i = j = 0; i < nd; i++){ //hprint(hout, "debug: art = %s\n
", d[i].name); if (d[i].name[0] == 'A'){ ents[j++] = s; s = strecpy(s, buf+l, d[i].name); *s++ = 0; } } *entsp = ents; *nentsp= j; free(d); return buf; } static void getidxreq(Req* req) { static char prevfmt[] = "%s

\n"; static char nextfmt[] = "%s

\n"; char* idx; char** ents; int nents; char* buf; Biobuf* ib; int i; int start; int n; char* s; idx = smprint("%s/%s/index.html", Top, req->group); start = 0; s = getarg(req, 'S', 0); if (s != nil) start=atoi(s); ib = Bopen(idx, OREAD); if (ib == nil) notfound("index"); headers(nil); outfilehdr(ib); buf = readindex(req, &ents, &nents); n = 0; for (i = nents-1; i >= 0; i--){ if (start != 0 && n == start){ hprint(hout, prevfmt , req->group, ((start - Nfront) < 0 ? 0 : (start - Nfront)), Mgoback); } if (n >= start && n < start+Nfront) outpostintro(req->group, ents[i], 2); if (n == start+Nfront){ hprint(hout, nextfmt, req->group, start + Nfront, Mgoonreading); break; } n++; } outfiletail(ib); Bterm(ib); free(idx); free(ents); free(buf); } static void outpost(int tab, char* g, char* f, Post* p) { char tabs[Nids+1]; char *np; char *sp; char* s; char* titlefmt= "%s%s\n

"; char* authfmt= "%s%s %s
\n"; char* otherfmt = "%s%s %s
\n"; char* replfmt = "%s%s\n"; memset(tabs, '\t', sizeof(tabs)); tabs[tab] = 0; if (tab <= 0){ if (p->title[0] != 0) hprint(hout, titlefmt, tabs, p->title); hprint(hout, authfmt, tabs, Mby, p->author); } else hprint(hout, otherfmt, tabs, p->author, Mwrote); sp = p->body; if (p->body[0] != 0){ while(sp != nil){ np = strchr(sp, '\n'); hprint(hout, "%s", tabs); if (np == nil) hprint(hout, "%s\n", sp); else{ hwrite(hout, sp, np-sp); hprint(hout, "\n"); np++; } sp = np; } } if (g != nil && f != nil){ s = id2str(p->ids, p->nids); hprint(hout, replfmt, tabs, g, f, s, Mreply); } hprint(hout, "


\n"); } static void outarticle(int tab, char* g, char* f, Post* p) { int i; outpost(tab, g, f, p); for (i = 0; i < p->nsons; i++) outarticle(tab+1, g, f, p->sons[i]); } static Post* openarticle(char* group, char* art) { Post* p; Biobuf* ib; ulong mt; ib = bopenfile(group, art, &mt); p = readarticle(ib, nil); if (p == nil){ Bterm(ib); notfound(art); } Bterm(ib); return p; } static void getartreq(Req* req) { Post* p; Biobuf* ib; ib = bopenfile(req->group, "article.html", nil); if (ib == nil) notfound("article.html"); req->file = getarg(req, 'a', 1); // Could locate previous and following article in index // to output links near the bottom. p = openarticle(req->group, req->file); headers(nil); outfilehdr(ib); outarticle(0, req->group, req->file, p); hprint(hout, "

%s\n", req->group, Mreturntoindex); outfiletail(ib); Bterm(ib); } static void outreplyform(Req* r, Post* p, char* form) { Biobuf* ib; char* id; ib = bopenfile(r->group, form, nil); if (ib == nil) notfound(form); outfilehdr(ib); if (p == nil || r->file == nil){ hprint(hout, "

%s

\n", Msendnew); hprint(hout, "
\n

", r->group); } else { hprint(hout, "

%s

\n
", Mreplyto);
		outpost(0, nil, nil, p);
		id = id2str(p->ids, p->nids);
		hprint(hout,
			"

\n

", r->group, r->file, id); free(id); } outfiletail(ib); Bterm(ib); } static void replyreq(Req* req) { Post* p; Post* top; char* id; int ids[Nids]; int nids; Biobuf* ib; char* form; p = nil; req->file = getarg(req, 'a', 0); if (req->file == nil) form = "post.html"; else { form = "reply.html"; id = getarg(req, 'i', 0); if (id == nil) badreq(); nids = parseid(id, ids, Nids); ib = bopenfile(req->group, req->file, nil); top = readarticle(ib, nil); if (top == nil) notfound(req->file); Bterm(ib); p = lookup(top, ids, nids); if (p == nil){ closepost(top); // BUG: leak: sons badreq(); } } headers(nil); outreplyform(req, p, form); } static void putpost(Req* r, Post* np) { static char buf[Maxpost]; char* e; Post* p; int fd; char* fname; char name[40]; if (r->file != nil){ p = openarticle(r->group, r->file); p = lookup(p, np->ids, np->nids); if (p == nil) notfound("previous article"); closepost(p); // BUG: leaks sons if (np->nids == Nids) failure(); np->ids[np->nids++] = truerand()%10000; fname = smprint("%s/%s/%s", Top, r->group, r->file); if (strstr(fname, "..")) badreq(); fd = open(fname, OWRITE); free(fname); } else { fname = smprint("%s/%s/INDEX", Top, r->group); fd = open(fname, OWRITE); if (fd < 0) notfound("INDEX"); free(fname); seprint(name, name+sizeof(name), "A%08uld", truerand()%10000000); fname = smprint("%s/%s/%s", Top, r->group, name); if (strstr(fname, "..")) badreq(); seek(fd, 0, 2); fprint(fd, "%s\n", name); close(fd); //fprint(2, "creating %s\n", fname); fd = create(fname, OWRITE, DMAPPEND|0666); free(fname); np->ids[0] = truerand()%10000; np->nids = 1; } if (fd < 0) notfound("post"); seek(fd, 0, 2); e = seprintpost(buf, buf+sizeof(buf), np); write(fd, buf, e - buf); close(fd); } static int checkbodyok(char* body) { char* ln; while(*body){ ln = strchr(body+1, '\n'); if (ln == nil) ln = body+strlen(body); if (ln - body > 80){ notice(Mbodywithlonglines); return 0; } body = ln; } return 1; } static void putreplyreq(Req* r) { Post* p; char* id; p = newpost(); id = getarg(r, 'i', 0); r->file = getarg(r, 'a', 0); p->title = getarg(r, 'T', 0); p->author= getarg(r, 'A', 1); p->body = getarg(r, 'B', 1); if(r->file == nil){ // new article if (!p->title) badreq(); if (!p->title[0]){ notice(Mrequiredfieldmissing); return; } } if (!p->author[0] || !p->body[0]){ notice(Mrequiredfieldmissing); return; } if (!checkbodyok(p->body)) return; if (id == nil){ if (r->file != nil) badreq(); p->nids = 0; } else{ if (r->file == nil) badreq(); p->nids = parseid(id, p->ids, Nids); } if (p->title == nil) p->title = strdup(""); if (p->body == nil) p->body = strdup(""); putpost(r, p); notice(Mclickheretoreturn, r->group); } static void searchreq(Req* r) { char* words[10]; int nwords; char* arg; char** ents; int nents; char* buf; char* fbuf; int i, j; char* fname; // All the Post has been parsed, // but for the extra from fields. arg = getarg(r, 'w', 1); nwords = tokenize(arg, words, nelem(words)); buf = readindex(r, &ents, &nents); for (i = 0; i < nents; i++){ fname = smprint("%s/%s/%s", Top, r->group, ents[i]); fbuf = readfile(fname); free(fname); if (fbuf != nil){ for (j = 0; j < nwords; j++){ if (cistrstr(fbuf, words[j]) == 0){ ents[i] = nil; break; } } free(fbuf); } else ents[i] = nil; } headers("search"); hprint(hout, "\n"); hprint(hout, "

%s

\n

\n", Msearchresults); for (i = 0; i < nents; i++){ if (ents[i] == nil) continue; outpostintro(r->group, ents[i], 2); } hprint(hout, "


%s" "\n", r->group, Mreturn); free(buf); free(ents); } static Opf ops[] = { { 'i', getidxreq }, { 'a', getartreq }, { 'r', replyreq }, { 'P', putreplyreq }, { 's', searchreq }, }; static void request(char* req) { Req r; int i; if (req == nil) req = Dfltreq; parsereq(req, &r); if (debug) fprint(2, "op %d, group %s\n", r.op, r.group); for (i = 0; i < nelem(ops); i++) if (r.op == ops[i].op){ ops[i].f(&r); break; } if (i == nelem(ops)) badreq(); } void main(int argc, char **argv) { Hio* hin; char* t; char* s; char* opts; fmtinstall('H', httpfmt); fmtinstall('U', hurlfmt); if (debug) dup(open("/dev/cons", OWRITE), 2); else close(2); hcon = init(argc, argv); hout = &hcon->hout; if(hparseheaders(hcon, HSTIMEOUT) < 0) exits("failed"); hcon->head.closeit = 1; if(strcmp(hcon->req.meth, "GET") != 0 && strcmp(hcon->req.meth, "HEAD") != 0) if(strcmp(hcon->req.meth, "POST") != 0){ hunallowed(hcon, "GET, HEAD, PUT"); exits("not allowed"); } if(hcon->head.expectother){ hfail(hcon, HExpectFail, nil); exits("failed"); } if(hcon->head.expectcont){ hprint(hout, "100 Continue\r\n"); hprint(hout, "\r\n"); hflush(hout); } if (strcmp(hcon->req.meth, "POST") == 0){ s = nil; hin = hbodypush(&hcon->hin, hcon->head.contlen, hcon->head.transenc); if(hin != nil){ alarm(15*60*1000); s = hreadbuf(hin, hin->pos); alarm(0); } if(s == nil){ hfail(hcon, HBadReq, nil); exits("failed"); } t = strchr(s, '\n'); if(t != nil) *t = '\0'; if (hcon->req.search != nil) opts = smprint("%s&%s", hcon->req.search, s); else opts = s; } else opts = hcon->req.search; vermaj = hcon->req.vermaj; truerand(); // open /dev/random before bind. bind("/usr/web", "/", MREPL); request(opts); hlflush(hout); hclose(hout); writelog(hcon, "200 group %ld %ld\n", hout->seek, hout->seek); exits(nil); }