#include "i.h" typedef struct NGchanitem NGchanitem; typedef struct ImageCache ImageCache; // Item sent allong ngchan struct NGchanitem { int req; ByteSource* bs; Netconn* nc; Channel* anschan; }; // In-memory cache of CImages struct ImageCache { CImage* imhd; // head (LRU) of cache chain (linked through CImage.next) CImage* imtl; // tail MRU) of cache chain int n; // size of chain int memused; // current total of image mem used by cached images int memlimit; // keep memused less than this int nlimit; // keep n less than this }; // Some constants enum { NCTimeout = 100000, // free NC slot after 100 seconds UBufsize = 40960, // initial buffer size for unknown lengths UEBufsize = 1024, // initial buffer size for unknown lengths, error responses Maxnthreads = 10, // max allowed config.nthreads }; Rune crlf[] = L"\r\n"; Rune sptab[] = L" \t"; // globals int bytesourceid = 0; // for allocating bytesource ids int netconnid = 0; // for allocating netconn ids int dbg; // config.dbg['d'] int warn; // dbg || config.dbg['w'] int dbgev; // config.dbg['e'] int dbgproto; // are we debugging netget protocol? int aborting; // in the process of aborting a get Netconn** netconns; // array of netconns int nnetconns; // current number of slots in netconns Channel* ngchan; // Channel of NGchanitem Channel* reqanschan; // Channel of ByteSource* Channel* abortanschan; // Channel of ByteSource* Config config; ResourceState startres; ImageCache imcache; // Track HTTP methods in i.h // (upper-case, since that's required in HTTP requests) Rune* hmeth[] = { L"GET", L"POST" }; // Track media types in charon.h // keep in alphabetical order Rune* mnames[] = { L"application/msword", L"application/octet-stream", L"application/pdf", L"application/postscript", L"application/rtf", L"application/vnd.framemaker", L"application/vnd.ms-excel", L"application/vnd.ms-powerpoint", L"application/x-unknown", L"audio/32kadpcm", L"audio/basic", L"image/cgm", L"image/g3fax", L"image/gif", L"image/ief", L"image/jpeg", L"image/png", L"image/tiff", L"image/x-bit", L"image/x-bit2", L"image/x-bitmulti", L"image/x-xbitmap", L"model/vrml", L"multipart/digest", L"multipart/mixed", L"text/css", L"text/enriched", L"text/html", L"text/javascript", L"text/plain", L"text/richtext", L"text/sgml", L"text/tab-separated-values", L"text/xml", L"video/mpeg", L"video/quicktime" }; // Track Charsets in i.h //(cf rfc1945 for other names, those we don't bother handling) Rune* chsetnames[] = { L"unknown", L"us-ascii", L"iso-8859-1", L"unicode-1-1-utf-8", L"unicode-1-1" }; // Track Netconn states in i.h Rune* ncstatenames[] = { L"free", L"idle", L"connect", L"gethdr", L"getdata", L"done", L"err" }; // Track major status values in i.h Rune* hsnames[] = { L"none", L"information", L"ok", L"redirect", L"request error", L"server error" }; // File extension to Media type lookup table // (keep sorted on file extension) static StringInt fileexttable[] = { {L"ai", ApplPostscript}, {L"au", AudioBasic}, {L"bit", ImageXBit}, {L"bit2", ImageXBit2}, {L"bitm", ImageXBitmulti}, {L"eps", ApplPostscript}, {L"gif", ImageGif}, {L"htm", TextHtml}, {L"html", TextHtml}, {L"jpe", ImageJpeg}, {L"jpeg", ImageJpeg}, {L"jpg", ImageJpeg}, {L"pdf", ApplPdf}, {L"ps", ApplPostscript}, {L"text", TextPlain}, {L"tif", ImageTiff}, {L"tiff", ImageTiff}, {L"txt", TextPlain} }; #define NFILEEXTS (sizeof(fileexttable)/sizeof(StringInt)) // Beginning of a JPEG file static uchar jpmagic[] = {0xFF, 0xD8, 0xFF, 0xE0, 0, 0, 'J', 'F', 'I', 'F', 0}; // Color lookup table static StringInt color_tab[] = { {L"aqua", 0x00FFFF}, {L"black", Black}, {L"blue", Blue}, {L"fuchsia", 0xFF00FF}, {L"gray", 0x808080}, {L"green", 0x008000}, {L"lime", 0x00FF00}, {L"maroon", 0x800000}, {L"navy", Navy}, {L"olive", 0x808000}, {L"purple", 0x800080}, {L"red", Red}, {L"silver", 0xC0C0C0}, {L"teal", 0x008080}, {L"white", White}, {L"yellow", 0xFFFF00} }; #define NCOLORS (sizeof(color_tab)/sizeof(StringInt)) // track Error codes in i.h static Rune* errphrases[] = { L"(no error)", L"Stopped", L"Unsupported scheme", L"Unexpected HTTP answer code", L"Redirect loop", L"Can't get file status", L"Read error", L"Write error", L"Unexpected end of file", L"Couldn't connect", L"FTP protocol error", L"FTP login failed", L"HTTP protocol error", L"Authorization failed", L"Unsupported image type", L"Not enough memory", L"Image encoding is bad" }; #define NERRPHRASES (sizeof(errphrases)/sizeof(Rune*)) static StringInt *targetmap; static int targetmapsize; static int ntargets; static void runnetconn(void *arg); static int strhash(Rune* s); static Transport* gettransport(int scheme); static Netconn* newnetconn(int id); static void makefree(Netconn* nc); static ByteSource* newbytesource(void); static void imcacheinit(void); static void setconfig(int argc, char** argv); static int setopt(Rune* key, int keylen, Rune* val, int vallen); static void targetmapinit(void); static int rnhandler(void* ureg, char* note); static void sendprocintr(int pid); static int countrefgo(void); // Initialize Chutils globals void iutilsinit(int argc, char** argv) { startres = curresstate(); // Setconfig needs urlinit() to have been done. // Rest of inits are done after setconfig, in case their // actions depend on configuration settings. // guiinit() is done in i.c. urlinit(); setconfig(argc, argv); transportinit(); lexinit(); buildinit(); layoutinit(); imginit(); eventinit(); imcacheinit(); targetmapinit(); gcinit(); threadnotify(rnhandler, 1); aborting = 0; nnetconns = 10; netconns = emallocz(sizeof(Netconn)*nnetconns); dbgproto = config.dbg['p']; dbg = config.dbg['d']; warn = config.dbg['w']; dbgev = config.dbg['e']; ngchan = chancreate(sizeof(NGchanitem), 0); reqanschan = chancreate(sizeof(ByteSource*), 0); abortanschan = chancreate(sizeof(ByteSource*), 0); if(ngchan == nil || reqanschan == nil || abortanschan == nil) fatalerror("can't create channels"); } // Make a ByteSource for given request, and make sure // that it is on the queue of some Netconn. // If don't have a transport for the request's scheme, // the returned bs will have err set. ByteSource* startreq(ReqInfo* req) { ByteSource* bs; ByteSource* bsans; NGchanitem ngi; bs = newbytesource(); bs->req = req; bs->refgo = 1; bs->refnc = 1; if(config.showprogress) sendprogress(bs->id, Pstart, 0, req->url->url); ngi.req = NGstartreq; ngi.bs = bs; ngi.nc = nil; ngi.anschan = reqanschan; if(send(ngchan, &ngi) < 0) finish(); if(recv(reqanschan, &bsans) < 0) finish(); return bs; } // Wait for some ByteSource to // have a state change that go hasn't seen yet. // There's a race between go doing a freebs // and netconn returning a statechange; // fix that race by only returning bs's that // still have refgo == 1. ByteSource* waitreq(void) { ByteSource* bs; NGchanitem ngi; ngi.req = NGwaitreq; do { ngi.bs = nil; ngi.nc = nil; ngi.anschan = reqanschan; if(send(ngchan, &ngi) < 0) finish(); if(recv(reqanschan, &bs) < 0) finish(); if(bs == nil) return nil; } while(bs->refgo == 0); return bs; } // Notify netget that goproc is finished with bs. // See waitreq comment about waitfreq/freebs race. void freebs(ByteSource* bs) { NGchanitem ngi; bs->refgo = 0; ngi.req = NGfreebs; ngi.bs = bs; ngi.nc = nil; ngi.anschan = reqanschan; if(send(ngchan, &ngi) < 0) finish(); if(recv(reqanschan, &bs) < 0) finish(); } // Tell netget to abort all gets. // This involves sending interrupts to any runnetconns that // may be stuck in I/O. // All bs's will then be returned to waitreq with an "aborted" // error, after which this function can return. void abortgo(void) { NGchanitem ngi; if(dbg) trace("abort go\n"); ngi.req = NGabort; ngi.bs = nil; ngi.nc = nil; ngi.anschan = abortanschan; if(send(ngchan, &ngi) < 0) finish(); if(recv(abortanschan, nil) < 0) finish(); } int forkrunnetconn(Netconn *nc, void **rncargs) { int id; assert(nc->rnpid == 0 && nc->rntid == 0); if(config.nthreads > 0) id = proccreate(runnetconn, (void*)rncargs, STACKSIZE); else id = threadcreate(runnetconn, (void*)rncargs, STACKSIZE); if(id < 0) return -1; nc->rntid = id; return 0; } // This runs as a separate thread. // It acts as a monitor to synchronize access to the Netconn data // structures, as a dispatcher to start runnetconn's as needed to // process work on Netconn queues, and as a notifier to let goproc // know when any ByteSources have advanced their state. void netget(void* arg) { int msg; int n; int i; int toldgo; ByteSource* bs; ByteSource* bsnil; Netconn* nc; Channel* c; Channel* pendingc; int waitpending; int maxconn; Netconn* x; int minqlen; Rune* sport; int port; int freen; int scheme; Rune* host; int err; int totlen; ByteSource* sendtopending; NGchanitem ngi; Transport* tpt; int gncs[Maxnthreads]; void* rncargs[2]; USED(arg); waitpending = 0; maxconn = config.nthreads; if(maxconn < 1) maxconn = 1; if(maxconn > Maxnthreads) maxconn = Maxnthreads; for(n = 0; n < nnetconns; n++) netconns[n] = newnetconn(n); bsnil = nil; pendingc = nil; sendtopending = nil; toldgo = 0; while(1) { mainloop_start: if(aborting) { if(toldgo && countrefgo() == 0) { if(send(abortanschan, nil) < 0) finish(); aborting = 0; } } if(recv(ngchan, &ngi) < 0) finish(); msg = ngi.req; bs = ngi.bs; nc = ngi.nc; c = ngi.anschan; switch(msg) { case NGstartreq: // bs has req filled in, and is otherwise in its initial state. // Find a suitable Netconn and add bs to its queue of work, // then send nil along c to let goproc continue. if(dbgproto) trace("Startreq BS=%d for %U\n", bs->id, bs->req->url); scheme = bs->req->url->scheme; host = Strndup(bs->req->url->host, bs->req->url->nhost); err = 0; tpt = nil; if(aborting) err = ERRaborted; else if(scheme < 0 || scheme >= TRANSMAX || transports[scheme] == nil) err = ERRunsupscheme; else tpt = transports[scheme]; if(err) bs->err = err; else { sport = Strndup(bs->req->url->port, bs->req->url->nport); if(sport == nil) port = (tpt->defaultport)(scheme); else port = Strtol(sport, nil, 10); i = 0; freen = -1; for(n = 0; n < nnetconns && (i < maxconn || freen == -1); n++) { nc = netconns[n]; if(nc->state == NCfree) { if(freen == -1) freen = n; } else if(!Strcmp(nc->host, host) && nc->port == port && nc->scheme == scheme && i < maxconn) { gncs[i++] = n; } } if(i < maxconn) { // use a new netconn for this bs if(freen == -1) { // need to make netconns bigger netconns = (Netconn**)erealloc(netconns, (freen + 10) * sizeof(Netconn*)); for(i = nnetconns; i < nnetconns+10; i++) netconns[i] = newnetconn(i); freen = nnetconns; nnetconns += 10; } nc = netconns[freen]; nc->rnpid = 0; nc->rntid = 0; nc->host = host; nc->port = port; nc->scheme = scheme; nc->qlen = 0; nc->ngcur = 0; nc->gocur = 0; nc->reqsent = 0; nc->pipeline = 0; nc->connected = 0; } else { // use existing netconn with fewest outstanding requests nc = netconns[gncs[0]]; if(maxconn > 1) { minqlen = nc->qlen - nc->gocur; for(i = 1; i < maxconn; i++) { x = netconns[gncs[i]]; if(x->qlen - x->gocur < minqlen) { nc = x; minqlen = x->qlen - x->gocur; } } } } if(nc->qlen == nc->qalloclen) { // need to make queue bigger nc->queue = (ByteSource**)erealloc(nc->queue, (nc->qalloclen + 10) * sizeof(ByteSource*)); nc->qalloclen += 10; } nc->queue[nc->qlen++] = bs; bs->net = nc; if(dbgproto) trace("Chose NC=%d for BS %d, qlen=%d\n", nc->id, bs->id, nc->qlen); if(nc->state == NCfree || nc->state == NCidle) { if(nc->connected) { nc->state = NCgethdr; if(dbgproto) trace("NC %d: starting runnetconn in gethdr state\n", nc->id); } else { nc->state = NCconnect; if(dbgproto) trace("NC %d: starting runnetconn in connect state\n", nc->id); } rncargs[0] = nc; rncargs[1] = tpt; if(forkrunnetconn(nc, rncargs) < 0){ trace("Can't start runnetconn!\n"); return; } } } if(send(c, &bsnil) < 0) finish(); break; case NGwaitreq: // goproc wants to be notified when some ByteSource // changes to a state that the goproc hasn't seen yet. // Send such a ByteSource along return channel c. if(dbgproto) trace("Waitreq\n"); assert(!waitpending); if(aborting) { if(dbgproto) trace("Send to waitreq\n"); send(c, &bsnil); toldgo = 1; goto mainloop_start; } for(n = 0; n < nnetconns; n++) { nc = netconns[n]; if(nc->state == NCfree) continue; if(nc->gocur < nc->qlen) { bs = nc->queue[nc->gocur]; if(!bs->refgo) continue; if(bs->err || (bs->hdr != nil && !bs->seenhdr) || (bs->edata > bs->lim) || (nc->gocur < nc->ngcur)) { if(dbgproto) trace("Send BS %d to waitreq\n", bs->id); if(send(c, &bs) < 0) finish(); goto mainloop_start; } } } if(dbgproto) trace("Waitpending\n"); waitpending = 1; pendingc = c; break; case NGfreebs: if(dbgproto) trace("Freebs BS=%d\n", bs->id); nc = bs->net; if(bs->refnc == 0) { if(nc != nil) nc->queue[nc->gocur] = nil; } if(nc != nil) { // can be nil if no transport was found nc->gocur++; if(dbgproto) trace("NC %d: gocur=%d, ngcur=%d, qlen=%d\n", nc->id, nc->gocur, nc->ngcur, nc->qlen); if(nc->gocur == nc->qlen && nc->ngcur == nc->qlen) { if(!nc->connected) makefree(nc); } } if(send(c, &bsnil) < 0) finish(); break; case NGstatechg: // Some runnetconn is telling us tht it changed the // state of nc. Send a nil along c to let it continue. if(dbgproto) trace("Statechg NC=%d, state=%S\n", nc->id, ncstatenames[nc->state]); sendtopending = nil; if(!aborting && waitpending && nc->gocur < nc->qlen) { bs = nc->queue[nc->gocur]; if(dbgproto) { totlen = 0; if(bs->hdr != nil) totlen = bs->hdr->length; trace("BS %d: havehdr=%d seenhdr=%d edata=%d lim=%d, length=%d\n", bs->id, bs->hdr != nil, bs->seenhdr, bs->edata, bs->lim, totlen); if(bs->err) trace(" err=%S\n", errphrase(bs->err)); } if(bs->refgo && (bs->err || (bs->hdr != nil && !bs->seenhdr) || (bs->edata > bs->lim))) sendtopending = bs; } if(nc->state == NCdone || nc->state == NCerr) { if(dbgproto) trace("NC %d: runnetconn finishing\n", nc->id); if(nc->ngcur >= nc->qlen) { trace("NC %d has ngcur=%d, qlen=%d\n", nc->id, nc->ngcur, nc->qlen); } assert(nc->ngcur < nc->qlen); bs = nc->queue[nc->ngcur]; bs->refnc = 0; if(bs->refgo == 0) nc->queue[nc->ngcur] = nil; nc->ngcur++; if(dbgproto) trace("NC %d: ngcur=%d\n", nc->id, nc->ngcur); nc->state = NCidle; nc->rnpid = 0; nc->rntid = 0; if(dbgproto) trace("NC %d: idle\n", nc->id); if(aborting) { /* free ByteSources! */ makefree(nc); } else if(nc->ngcur < nc->qlen) { if(nc->connected) { nc->state = NCgethdr; if(dbgproto) trace("NC %d: starting runnetconn in gethdr state\n", nc->id); } else { nc->state = NCconnect; if(dbgproto) trace("NC %d: starting runnetconn in connect state\n", nc->id); } rncargs[0] = nc; rncargs[1] = transports[nc->scheme]; if(forkrunnetconn(nc, rncargs) < 0){ trace("Can't start runnetconn!\n"); return; } } else if(!nc->connected) makefree(nc); } if(dbgproto) trace("Send reply to chan 0x%p\n", c); send(c, &bsnil); if(dbgproto) trace("done sending reply to chan 0x%p\n", c); if(sendtopending != nil) { if(dbgproto) trace("Send BS %d to pending waitreq\n", bs->id); send(pendingc, &sendtopending); waitpending = 0; } break; case NGabort: if(dbgproto) trace("NGabort received\n"); aborting = 1; if(waitpending) { if(dbgproto) trace("Send to pending waitreq\n"); send(pendingc, &bsnil); waitpending = 0; toldgo = 1; } else toldgo = !countrefgo(); for(n = 0; n < nnetconns; n++) { nc = netconns[n]; if(nc->rntid != 0) { if(config.nthreads != 0) sendprocintr(nc->rnpid); /* else we can't interrupt */ } } break; } } } // Return number of ByteSources in netget queues // that have refgo static int countrefgo(void) { int ans, i, n; Netconn* nc; ByteSource* bs; ans = 0; for(n = 0; n < nnetconns; n++) { nc = netconns[n]; if(nc->state != NCfree) { for(i = 0; i < nc->qlen; i++) { bs = nc->queue[i]; if(bs == nil) continue; if(bs->refgo) ans++; } } } return ans; } static void sendprocintr(int pid) { int fd; char buf[100]; if(dbgproto) trace("proc %d sending rnintr to proc %d\n", getpid(), pid); sprint(buf, "/proc/%d/note", pid); fd = open(buf, OWRITE); if(fd >= 0) { write(fd, "rnintr", 6); close(fd); } } static int rnhandler(void* ureg, char* note) { USED(ureg); if(strncmp(note, "rnintr", 6) == 0) { if(dbgproto) trace("proc %d got rnintr\n", getpid()); return 1; } return 0; } // A separate proc, to handle ngcur request of transport. // The arguments should be: {Netconn*, Transport*}. // If config.nthreads == 0, it is run as thread rather than proc // (for debugging). static void runnetconn(void* arg) { Netconn* nc; Transport* tpt; int totlen; Channel* ach; ByteSource* bs; ByteSource* bsans; NGchanitem ngi; void** args; if(config.nthreads != 0) meminit(GRnet); args = (void**)arg; nc = (Netconn*)args[0]; nc->rnpid = getpid(); tpt = (Transport*)args[1]; ach = chancreate(sizeof(ByteSource*), 0); assert(nc->ngcur < nc->qlen); bs = nc->queue[nc->ngcur]; ngi.req = NGstatechg; ngi.bs = nil; ngi.nc = nc; ngi.anschan = ach; bs->data = nil; if(nc->state == NCconnect) { (tpt->connect)(nc, bs); if(bs->err || aborting) goto finished; nc->state = NCgethdr; } assert(nc->state == NCgethdr && nc->connected); if(config.showprogress) sendprogress(bs->id, Pconnected, 0, nil); // Write enough requests so that nc->ngcur header //will be next to be retrieved while(nc->reqsent < nc->qlen) { (tpt->writereq)(nc, nc->queue[nc->reqsent]); if(aborting) goto finished; if(nc->queue[nc->reqsent]->err) { if(nc->reqsent > nc->ngcur) { nc->queue[nc->reqsent]->err = 0; break; } else goto finished; } nc->reqsent++; if(nc->reqsent >= nc->ngcur + 1 || !nc->pipeline) break; } assert(nc->reqsent > nc->ngcur); // Get the header (tpt->gethdr)(nc, bs); if(bs->err || aborting) goto finished; assert(bs->hdr != nil); if(config.showprogress) sendprogress(bs->id, Phavehdr, 0, nil); totlen = bs->hdr->length; if(totlen > 0) { nc->state = NCgetdata; if(send(ngchan, &ngi) == -1) goto cleanup; if(recv(ach, &bsans) == -1) goto cleanup; bs->data = (uchar*)emalloc(totlen); bs->dalloclen = totlen; while(bs->edata < totlen) { (tpt->getdata)(nc, bs); if(bs->err || aborting) goto finished; if(config.showprogress) sendprogress(bs->id, Phavedata, 100*bs->edata/totlen, nil); if(send(ngchan, &ngi) == -1) goto cleanup; if(recv(ach, &bsans) == -1) goto cleanup; } } else if(totlen == -1) { // Unknown length. // To simplify consumer semantics, we want bs->data to // not change underfoot, so for now, simply accumlate // everything before telling consumer about a state change. // // Report progress percentage based on current totlen (wrong // of course, but at least shows trend) if(bs->hdr->code == HCOk || bs->hdr->code == HCOkNonAuthoritative) totlen = UBufsize; else totlen = UEBufsize; nc->state = NCgetdata; bs->hdr->length = 100000000; // avoid BS free during following loop if(send(ngchan, &ngi) == -1) goto cleanup; if(recv(ach, &bsans) == -1) goto cleanup; bs->data = (uchar*)emalloc(totlen); bs->dalloclen = totlen; while(1) { (tpt->getdata)(nc, bs); if(aborting) goto finished; if(config.showprogress) sendprogress(bs->id, Phavedata, 100*bs->edata/totlen, nil); if(bs->err) { // assume EOF bs->data = (uchar*)erealloc(bs->data, bs->edata); bs->dalloclen = bs->edata; bs->err = 0; bs->hdr->length = bs->edata; nc->connected = 0; break; } if(bs->edata == totlen) { totlen *= 2; bs->data = (uchar*)erealloc(bs->data, totlen); bs->dalloclen = totlen; } } } nc->state = NCdone; if(config.showprogress) sendprogress(bs->id, Phavedata, 100, nil); finished: if(aborting) bs->err = ERRaborted; if(bs->err) { nc->state = NCerr; nc->connected = 0; /* * If we're aborting, then the event loop is stuck in abortgo * waiting for us to finish. Don't update the progress meter, * since that requires having the event loop. */ if(!aborting && config.showprogress) sendprogress(bs->id, Perr, 0, errphrase(bs->err)); } if(send(ngchan, &ngi) == -1) goto cleanup; recv(ach, &bsans); if(dbgproto) trace("runnetconn in proc %d exiting\n", getpid()); threadexits(""); cleanup: if(aborting) goto finished; if(dbgproto) trace("runnetconn in proc %d exiting\n", getpid()); threadexits(""); } // Allocate a new Netconn, set its id and all other fields to "unused" state static Netconn* newnetconn(int id) { Netconn* nc; nc = (Netconn*)emalloc(sizeof(Netconn)); nc->id = id; nc->qalloclen = 10; nc->queue = (ByteSource**)emalloc(nc->qalloclen*sizeof(ByteSource*)); makefree(nc); return nc; } // Reset the fields of nc so that it is free to service another connection to anywhere. static void makefree(Netconn* nc) { if(dbgproto) trace("NC %d: free\n", nc->id); nc->state = NCfree; nc->rnpid = 0; nc->host = nil; nc->port = 0; nc->dfd = -1; nc->cfd = -1; nc->qlen = 0; nc->gocur = 0; nc->ngcur = 0; nc->reqsent = 0; nc->pipeline = 0; nc->connected = 0; nc->tstate = 0; nc->tn1 = 0; nc->tn2 = 0; nc->tbuf = nil; nc->tbuflen = 0; memset(nc->queue, 0, nc->qalloclen*sizeof(ByteSource*)); } // Allocate a new ByteSource, and set its fields to initial state. static ByteSource* newbytesource(void) { ByteSource* bs; bs = (ByteSource*)emallocz(sizeof(ByteSource)); bs->id = bytesourceid++; return bs; } // Return an ByteSource that is completely filled, from string s. ByteSource* newstringbytesource(Rune* s) { ByteSource* bs; Header* hdr; uchar* a; int n; n = Strlen(s); a = (uchar*)emalloc(n*sizeof(Rune)); memmove(a, s, n*sizeof(Rune)); hdr = newheader(); hdr->code = HCOk; hdr->length = n; hdr->mtype = TextHtml; hdr->chset = Unicode; bs = newbytesource(); bs->hdr = hdr; bs->data = a; bs->dalloclen = n*sizeof(Rune); bs->edata = bs->dalloclen; bs->refgo = 1; bs->seenhdr = 1; return bs; } // Allocate a new ReqInfo structure, fill it in, and return. ReqInfo* newreqinfo(ParsedUrl* url, int method, uchar* body, int bodylen, Rune* auth, int target) { ReqInfo* r; r = (ReqInfo*)emalloc(sizeof(ReqInfo)); r->url = url; r->method = method; r->body = body; r->bodylen = bodylen; r->auth = auth; r->target = target; return r; } // Allocate a new MaskedImage, with im for the image field. MaskedImage* newmaskedimage(Image* im) { MaskedImage* m; m = (MaskedImage*)emallocz(sizeof(MaskedImage)); m->im = im; return m; } // Allocate a new CImage, with the give src, width, and height. // Start with a reference count of 1. CImage* newcimage(ParsedUrl* src, int width, int height) { CImage* c; c = (CImage*)emalloc(sizeof(CImage)); c->src = src; c->actual = nil; if(src != nil) c->imhash = strhash(src->host) + strhash(src->path); else c->imhash = 0; c->width = width; c->height = height; c->refcnt = 1; c->next = nil; c->mims = nil; c->mimslen = 0; return c; } // Decrement the refcnt, and free when it reaches 0. void freecimage(CImage* c) { int i; MaskedImage* m; if(--c->refcnt <= 0) { if(c->mims != nil) { for(i = 0; i < c->mimslen; i++) { m = c->mims[i]; if(m->im != nil) freeimage(m->im); if(m->mask != nil) freeimage(m->mask); } } } } // Return true if Cimages a and b represent the same image. // As well as matching the src urls, the specified widths and heights must match too. // (Widths and heights are specified if at least one of those is not zero.) int cimagematch(CImage* a, CImage* b) { if(a->imhash == b->imhash) { if(urlequal(a->src, b->src)) { return (a->width == 0 || b->width == 0 || a->width == b->width) && (a->height == 0 || b->height == 0 || a->height == b->height); // (above is not quite enough: should also check that don't have // situation where one has width set, not height, and the other has reverse, // but it is unusual for an image to have a spec in only one dimension anyway) } } return 0; } // Return approximate number of bytes due to Images in ci int cimagebytes(CImage* ci) { int tot, i; MaskedImage* mim; Image* dim; tot = 0; for(i = 0; i < ci->mimslen; i++) { mim = ci->mims[i]; dim = mim->im; if(dim != nil) tot += ((dim->r.max.x-dim->r.min.x)>>(3-log2[dim->depth])) * (dim->r.max.y-dim->r.min.y); dim = mim->mask; if(dim != nil) tot += ((dim->r.max.x-dim->r.min.x)>>(3-log2[dim->depth])) * (dim->r.max.y-dim->r.min.y); } return tot; } // Call this after initial windows have been made, // so that resetlimits() will exclude the images for those // windows from the available memory. static void imcacheinit(void) { memset(&imcache, 0, sizeof(imcache)); imcacheresetlimits(); } // Call resetlimits when amount of non-image-cache image // memory might have changed significantly (e.g., on main window resize). void imcacheresetlimits(void) { int avail; ResourceState rs; rs = curresstate(); avail = rs.memavail; avail = 6*avail/10; // allow 40% slop for nonimage data and other applications imcache.memlimit = config.imagecachemem; if(imcache.memlimit > avail) imcache.memlimit = avail; imcache.nlimit = config.imagecachenum; imcacheneed(0); // if resized, perhaps need to shed some images } // Look for a CImage matching ci, and if found, move it // to the tail position (i.e., MRU) CImage* imcachelook(CImage* ci) { CImage* ans; CImage* prev; CImage* i; ans = nil; prev = nil; for(i = imcache.imhd; i != nil; i = i->next) { if(cimagematch(i, ci)) { if(imcache.imtl != i) { if(prev != nil) prev->next = i->next; else imcache.imhd = i->next; i->next = nil; imcache.imtl->next = i; imcache.imtl = i; } ans = i; break; } prev = i; } return ans; } // Call this to add ci as MRU of cache chain (should only call if // it is known that a ci with same image isn't already there). // Update imcache.memused. // Assume imcacheneed has been called to ensure that neither // memlimit nor nlimit will be exceeded. void imcacheadd(CImage* ci) { ci->next = nil; if(imcache.imhd == nil) imcache.imhd = ci; else imcache.imtl->next = ci; imcache.imtl = ci; imcache.memused += cimagebytes(ci); imcache.n++; } // Delete least-recently-used image in image cache // and update memused and n. void imcachedeletelru(void) { CImage* ci; ci = imcache.imhd; if(ci != nil) { imcache.imhd = ci->next; if(imcache.imhd == nil) { imcache.imtl = nil; imcache.memused = 0; } else imcache.memused -= cimagebytes(ci); freecimage(ci); imcache.n--; } } void imcacheclear(void) { while(imcache.imhd != nil) imcachedeletelru(); } int imcacheneed(int nbytes) { while(imcache.n >=imcache.nlimit || imcache.memused + nbytes > imcache.memlimit) { if(imcache.imhd == nil) return 0; imcachedeletelru(); } return 1; } // Return new Header with default values for fields Header* newheader(void) { Header* h; h = (Header*)emalloc(sizeof(Header)); h->code = HCOk; h->actual = nil; h->base = nil; h->location = nil; h->length = -1; h->mtype = UnknownType; h->chset = UnknownCharset; h->msg = nil; h->refresh = nil; h->chal = nil; h->warn = nil; return h; } // Set the mtype (and possibly chset) fields of h based on (in order): // first bytes of file, if unambigous // file name extension // first bytes of file, even if ambigous (guess) // if all else fails, then leave as UnknownType. // If it's a text type, also set the chset. // (HTTP Transport will try to use Content-Type first, and call this if that // doesn't work; other Transports will have to rely on this "guessing" function.) void setmediatype(Header* h, Rune* name, uchar* first, int firstlen) { int n; int mt; int i; Rune* s; int val; Rune* ext; Rune* file; uchar b; int nctl; int ntophalf; n = firstlen; mt = UnknownType; for(i = 0; i < n; i++) if(!isspace(first[i])) break; if(n - i >= 6) { s = toStr(first, 6, US_Ascii); if(!Strcmp(s, L"") || !Strcmp(s, L"") || !Strcmp(s, L"mtype = TextHtml; } else if(!Strcmp(s, L"mtype = ImageXXBitmap; } if(h->mtype == UnknownType) { if(i == 0 && n >= sizeof(jpmagic)) { for(; i < sizeof(jpmagic); i++) if(jpmagic[i] > (uchar)0 && first[i] != jpmagic[i]) break; if(i == sizeof(jpmagic)) mt = ImageJpeg; } } } if(mt == UnknownType && name != nil) { // Try file name extension file = Strrclass(name, L"/"); if(file == nil) file = name; else file++; if(*file != 0) { ext = Strrclass(file, L"."); if(ext != nil) { ext++; if(lookup(fileexttable, NFILEEXTS, ext, Strlen(ext), &val)) mt = val; } } } if(mt == UnknownType || (mt >= TextHtml && mt <= TextSgml)) { // Try file statistics nctl = 0; ntophalf = 0; if(n > 1024) n = 1024; for(i = 0; i < n; i++) { b = first[i]; if(iscntrl(b)) nctl++; if(b >= 128) ntophalf++; } if(nctl < n/100 && ntophalf < n/10) { if(mt == UnknownType) mt = TextPlain; if(ntophalf == 0) h->chset = US_Ascii; else h->chset = ISO_8859_1; } else if(mt == UnknownType) { if(n == 0) { mt = TextHtml; h->chset = ISO_8859_1; } else mt = ApplOctets; } else h->chset = ISO_8859_1; } h->mtype = mt; } void printheader(Header* h) { Rune* mtype; Rune* chset; mtype = L"?"; if(h->mtype >= 0 && h->mtype < NMEDIATYPES) mtype = mnames[h->mtype]; chset = L"?"; if(h->chset >= 0 && h->chset < NCHARSETS) chset = chsetnames[h->chset]; trace("code=%d (%S) length=%d mtype=%S chset=%S\n", h->code, hcphrase(h->code), h->length, mtype, chset); if(h->base != nil) trace(" base=%U\n", h->base); if(h->location != nil) trace(" location=%U\n", h->location); if(h->refresh != nil) trace(" refresh=%S\n", h->refresh); if(h->chal != nil) trace(" chal=%S\n", h->chal); if(h->warn != nil) trace(" warn=%S\n", h->warn); } #define PAGESIZE 4096 ResourceState curresstate(void) { int n; static int fd = -1; char* p; ResourceState r; uchar buf[SMALLBUFSIZE]; r.ms = (int)cputime(); r.mem = 1*1024*1024; r.memavail = 10*1024*1024; if(fd < 0) fd = open("/dev/swap", OREAD); if(fd >= 0) { seek(fd, 0, 0); n = read(fd, buf, (sizeof buf) - 1); if(n > 3) { buf[n] = 0; r.mem = strtol((char*)buf, &p, 10) * PAGESIZE; if(p != nil && *p == '/') { r.memavail = strtol((char*)(p+1), &p, 10) * PAGESIZE; } } } return r; } ResourceState resstatesince(ResourceState rnew, ResourceState rold) { ResourceState r; r.ms = rnew.ms - rold.ms; r.mem = rnew.mem - rold.mem; r.memavail = rnew.memavail - rold.memavail; return r; } void resstateprint(ResourceState r, char* msg) { int d, dms; d = r.ms/1000; dms = r.ms % 1000; trace("%s:\n\ttime: %d.%#.3ds; memory: %dK, available memory %dK\n", msg, d, dms, r.mem/1024, r.memavail/1024); } // Decide what to do based on Header and whether this is // for the main entity or not, and the number of redirections-so-far. // Return value is "use"; other return values put in *perror, *pchallenge, and *predir. // Action to do is: // If use==1, use the entity else drain its byte source. // If *perror != nil, mesg was put in progress bar // If (pchallenge != nil, get auth info and make new request with auth // Else if *predir != nil, make a new request with redir for url // // (if challenge or redir is non-nil, use will be 0) int hdraction(ByteSource* bs, int ismain, int nredirs, int* perror, Rune** pchallenge, ParsedUrl** predir) { int use; int error; Rune* challenge; ParsedUrl* redir; Header* h; int code; use = 1; error = 0; challenge = nil; redir = nil; h = bs->hdr; assert(h != nil); bs->seenhdr = 1; code = h->code; switch(code/100) { case HSOk: if(code != HCOk) error = ERRunexphscode; break; case HSRedirect: if(h->location != nil) { redir = h->location; if(redir->scheme == NOSCHEME) redir = makeabsoluteurl(redir, h->base); if(dbg) trace("redirect %U to %U\n", h->actual, redir); if(nredirs >= Maxredir) { redir = nil; error = ERRredirloop; } else use = 0; } break; case HSError: if(code == HCUnauthorized && h->chal != nil) { challenge = h->chal; use = 0; } else { error = code; use = ismain; } break; case HSServererr: error = code; use = ismain; break; default: error = ERRunexphscode; use = 0; break; } if(error && config.showprogress) sendprogress(bs->id, Perr, 0, errphrase(error)); *perror = error; *pchallenge = challenge; *predir = redir; return use; } void sendprogress(int bsid, int state, int pcnt, Rune* s) { EV prog; prog.tag = EVprogresstag; prog.genframeid = -1; // TODO: pass in correct frameid prog.u.progress.bsid = bsid; prog.u.progress.state = state; prog.u.progress.pcnt = pcnt; prog.u.progress.s = s; send(evchan, &prog); } // Read a line up to and including cr/lf (be tolerant and allowing missing cr). // Look first in buf[*pbstart:*pbend], and if that isn't sufficient to get whole line, // refill buf from fd as needed. // Since most of the time the needed line will be in the passed buffer, we sometimes // return pointers into that buffer. We only allocated the answer if the line straddles // buffer reads. // // Return values: // getline returns 1 if got eof, else 0. // *pans points to line, *panslen points to its length (not including cr/lf). // *pbstart and *pbend are updated to new valid portion of buf (after cr/lf). int getline(int fd, uchar* buf, int bufsize, int* pbstart, int* pbend, uchar** pans, int* panslen) { int i, k, eof, bstart, bend; int anslen, lastlen; uchar* ans; uchar* last; ans = nil; anslen = 0; eof = 0; bstart = *pbstart; bend = *pbend; while(1) { for(i = bstart; i < bend; i++) { if(buf[i] == '\n') { k = i; if(k > bstart && buf[k - 1] == '\r') k--; last = buf+bstart; lastlen = k-bstart; bstart = i + 1; goto mainloop_done; } } k = bend-bstart; if(k > 0) { if(anslen > 0) ans = (uchar*)erealloc(ans, anslen+k); else ans = (uchar*)emalloc(k); memmove(ans+anslen, buf+bstart, k); anslen += k; } last = nil; lastlen = 0; bstart = 0; bend = read(fd, buf, bufsize); if(bend <= 0) { eof = 1; bend = 0; break; } } mainloop_done: if(anslen == 0) { ans = last; anslen = lastlen; } else { if(lastlen > 0) { ans = (uchar*)erealloc(ans, anslen+lastlen); memmove(ans+anslen, last, lastlen); anslen += lastlen; } } *pans = ans; *panslen = anslen; *pbstart = bstart; *pbend = bend; //trace("*pans=%d, *panslen=%d, *pbstart=%d, *pbend=%d, eof=%d\n", ans, anslen, bstart, bend, eof); //if(anslen > 0) { write(1, ans, anslen); trace("\n"); } return eof; } // Look (linearly) through a for s[0:slen] (case insensitive); return its index if found, else -1. int Strlookup(Rune** a, int n, Rune* s, int slen) { int i; for(i = 0; i < n; i++) if(!Strncmpci(s, slen, a[i])) return i; return -1; } // Return string representation of HTTP code. // Note: returned values are constants, and should not be freed. Rune* hcphrase(int code) { Rune* ans; switch(code) { case HCContinue: ans = L"Continue"; break; case HCSwitchProto: ans = L"Switching Protcols"; break; case HCOk: ans = L"Ok"; break; case HCCreated: ans = L"Created"; break; case HCAccepted: ans = L"Accepted"; break; case HCOkNonAuthoritative: ans = L"Non-Authoratative Information"; break; case HCNoContent: ans = L"No content"; break; case HCResetContent: ans = L"Reset content"; break; case HCPartialContent: ans = L"Partial content"; break; case HCMultipleChoices: ans = L"Multiple choices"; break; case HCMovedPerm: ans = L"Moved permanently"; break; case HCMovedTemp: ans = L"Moved temporarily"; break; case HCSeeOther: ans = L"See other"; break; case HCNotModified: ans = L"Not modified"; break; case HCUseProxy: ans = L"Use proxy"; break; case HCBadRequest: ans = L"Bad request"; break; case HCUnauthorized: ans = L"Unauthorized"; break; case HCPaymentRequired: ans = L"Payment required"; break; case HCForbidden: ans = L"Forbidden"; break; case HCNotFound: ans = L"Not found"; break; case HCMethodNotAllowed: ans = L"Method not allowed"; break; case HCNotAcceptable: ans = L"Not Acceptable"; break; case HCProxyAuthRequired: ans = L"Proxy authentication required"; break; case HCRequestTimeout: ans = L"Request timed-out"; break; case HCConflict: ans = L"Conflict"; break; case HCGone: ans = L"Gone"; break; case HCLengthRequired: ans = L"Length required"; break; case HCPreconditionFailed: ans = L"Precondition failed"; break; case HCRequestTooLarge: ans = L"Request entity too large"; break; case HCRequestURITooLarge: ans = L"Request-URI too large"; break; case HCUnsupportedMedia: ans = L"Unsupported media type"; break; case HCRangeInvalid: ans = L"Requested range not valid"; break; case HCExpectFailed: ans = L"Expectation failed"; break; case HCServerError: ans = L"Internal server error"; break; case HCNotImplemented: ans = L"Not implemented"; break; case HCBadGateway: ans = L"Bad gateway"; break; case HCServiceUnavailable: ans = L"Service unavailable"; break; case HCGatewayTimeout: ans = L"Gateway time-out"; break; case HCVersionUnsupported: ans = L"HTTP version not supported"; break; case HCRedirectionFailed: ans = L"Redirection failed"; break; default: ans = L"Unknown code"; break; } return ans; } // Error codes can be either one of the ERR... values or one of the HC... values Rune* errphrase(int code) { if(code >=0 && code < NERRPHRASES) return errphrases[code]; return hcphrase(code); } // Make a StringInt table out of a[0:n], mapping each string // to its index. Check that entries are in alphabetical order. // These tables use the real malloc(), and are never freed. StringInt* makestrinttab(Rune** a, int n) { StringInt* ans; int i; ans = (StringInt*)malloc(n * sizeof(StringInt)); for(i = 0; i < n; i++) { ans[i].key = a[i]; ans[i].val = i; assert(i == 0 || Strcmp(a[i], a[i - 1]) >= 0); } return ans; } // Use targetmap array to keep track of name <-> targetid mapping. // Use real malloc(), and never free static void targetmapinit(void) { targetmapsize = 10; targetmap = (StringInt*)malloc(targetmapsize*sizeof(StringInt)); memset(targetmap, 0, targetmapsize*sizeof(StringInt)); targetmap[0].key = L"_top"; targetmap[0].val = FTtop; targetmap[1].key = L"_self"; targetmap[1].val = FTself; targetmap[2].key = L"_parent"; targetmap[2].val = FTparent; targetmap[3].key = L"_blank"; targetmap[3].val = FTblank; ntargets = 4; } int targetid(Rune* s) { int i; int n; n = Strlen(s); if(n == 0) return FTself; for(i = 0; i < ntargets; i++) if(Strcmp(s, targetmap[i].key) == 0) return targetmap[i].val; if(i >= targetmapsize) { targetmapsize += 10; targetmap = (StringInt*)realloc(targetmap, targetmapsize*sizeof(StringInt)); } targetmap[i].key = (Rune*)malloc((n+1)*sizeof(Rune)); memmove(targetmap[i].key, s, (n+1)*sizeof(Rune)); targetmap[i].val = i; ntargets++; return i; } Rune* targetname(int targid) { int i; for(i = 0; i < ntargets; i++) if(targetmap[i].val == targid) return targetmap[i].key; return L"?"; } // Use logtime when only care about time stamps void logtime(char* msg, int data) { trace("%s: %d %d\n", msg, (int)cputime() - startres.ms, data); } // Convert HTML color spec to RGB value, returning dflt if can't. // Argument is supposed to be a valid HTML color, or "". // Return the RGB value of the color, using dflt if s // is nil or an invalid color. int color(Rune* s, int dflt) { int v; Rune* rest; if(s == nil) return dflt; if(lookup(color_tab, NCOLORS, s, Strlen(s), &v)) return v; if(s[0] == '#') s++; v = Strtol(s, &rest, 16); if(*rest == 0) return v; return dflt; } static int strhash(Rune* s) { int hash; enum { prime = 8388617 }; hash = 0; if(s != nil) { while(*s != 0) hash = ((hash%prime) << 7) + *s++; } return hash; } // Set up config global to defaults, then try to read user-specifiic // config data from /usr//lib/iconfig, then try to // override from command line arguments. static void setconfig(int argc, char** argv) { Rune* user; int fd; int n; Rune* line; int linelen; Rune* key; int keylen; Rune* val; int vallen; int i; int j; uchar* aline; int alinelen; int eof; int cfgio; Rune* a; Rune* b; char* s; uchar buf[BIGBUFSIZE]; config.userdir = Strdup(L"/"); config.starturl = makeurl(L"file:/lib/i/start.html", 0); config.homeurl = copyurl(config.starturl); config.httpproxy = nil; config.defaultwidth = 800; config.defaultheight = 800; config.x = 0; config.y = 0; config.nocache = 0; config.maxstale = 0; config.imagelvl = ImgFull; config.imagecachenum = 60; config.imagecachemem = 100000000; config.docookies = 0; config.doscripts = 0; config.saveauthinfo = 0; config.showprogress = 1; config.usecci = 0; config.httpminor = 0; config.agentname = Strdup(L"i/2.0 (Plan 9)"); config.nthreads = 1; config.dbgfile = nil; user = nil; fd = open("/dev/user", OREAD); if(fd != -1) { n = read(fd, buf, sizeof(buf)); if(n > 0) user = toStr(buf, n, UTF_8); close(fd); } if(user != nil) { config.userdir = Strdup3(L"/usr/", user, L"/lib"); snprint((char*)buf, sizeof(buf), "%S/iconfig", config.userdir); cfgio = open((char*)buf, OREAD); if(cfgio != -1) { i = 0; j = 0; while(1) { eof = getline(cfgio, buf, sizeof(buf), &i, &j, &aline, &alinelen); if(eof) break; if(alinelen == 0) continue; line = toStr(aline, alinelen, UTF_8); linelen = Strlen(line); if(alinelen > 0 && line[0] != '#') { splitl(line, linelen, L" \t=", &key, &keylen, &val, &vallen); if(keylen != 0) { if(vallen != 0) { a = Strnclass(val, L"^ \t=", vallen); if(a == nil) vallen = 0; else { vallen -= (a-val); val = a; a = Strnclass(val, L"#\r\n", vallen); if(a != nil) vallen = a-val; } } if(!setopt(key, keylen, val, vallen)) trace("couldn't set option from line '%S'\n", line); } } } close(cfgio); } } argc--; argv++; while(argc > 0) { s = argv[0]; if(*s == 0) continue; else if(s[0] == '-') { a = toStr((uchar*)(s+1), strlen(s+1), UTF_8); b = nil; if(argc > 1) { b = toStr((uchar*)argv[1], strlen(argv[1]), UTF_8); if(b[0] != '-') { argc--; argv++; } } if(!setopt(a, Strlen(a), b, Strlen(b))) trace("couldn't set option from arg '%s'\n", s); } else { if(argc == 1) { a = toStr((uchar*)s, strlen(s), UTF_8); if(!setopt(L"starturl", 8, a, Strlen(a))) trace("couldn't set starturl from arg '%s'\n", s); } else trace("ignoring unknown option %s\n", s); } argc--; argv++; } } static int setopt(Rune* key, int keylen, Rune* val, int vallen) { int ok; int v; int c; int i; Rune* s; ok = 1; if(vallen == 0) { s = nil; v = 1; } else { s = Strndup(val, vallen); v = Strtol(s, nil, 0); } if(!Strncmpci(key, keylen, L"userdir")) { config.userdir = Strdup(s); } else if(!Strncmpci(key, keylen, L"starturl")) { if(vallen != 0) { config.starturl = makeurl(s, 1); assert(config.starturl != nil); } else ok = 0; } else if(!Strncmpci(key, keylen, L"homeurl")) { if(vallen != 0) { config.homeurl = makeurl(s, 1); } else ok = 0; } else if(!Strncmpci(key, keylen, L"httpproxy")) { if(vallen != 0) { config.httpproxy = makeurl(s, 1); } else config.httpproxy = nil; } else if(!Strncmpci(key, keylen, L"defaultwidth") || !Strncmpci(key, keylen, L"width")) { if(v > 200) config.defaultwidth = v; else ok = 0; } else if(!Strncmpci(key, keylen, L"defaultheight") || !Strncmpci(key, keylen, L"height")) { if(v > 100) config.defaultheight = v; else ok = 0; } else if(!Strncmpci(key, keylen, L"x")) { config.x = v; } else if(!Strncmpci(key, keylen, L"y")) { config.y = v; } else if(!Strncmpci(key, keylen, L"nocache")) { config.nocache = v; } else if(!Strncmpci(key, keylen, L"maxstale")) { config.maxstale = v; } else if(!Strncmpci(key, keylen, L"imagelvl")) { config.imagelvl = v; } else if(!Strncmpci(key, keylen, L"imagecachenum")) { config.imagecachenum = v; } else if(!Strncmpci(key, keylen, L"imagecachemem")) { config.imagecachemem = v; } else if(!Strncmpci(key, keylen, L"docookies")) { config.docookies = v; } else if(!Strncmpci(key, keylen, L"doscripts")) { config.doscripts = v; } else if(!Strncmpci(key, keylen, L"saveauthinfo")) { config.saveauthinfo = v; } else if(!Strncmpci(key, keylen, L"showprogress")) { config.showprogress = v; } else if(!Strncmpci(key, keylen, L"usecci")){ config.usecci = v; } else if(!Strncmpci(key, keylen, L"http")) { if(!Strncmpci(val, vallen, L"1.1")) config.httpminor = 1; else config.httpminor = 0; } else if(!Strncmpci(key, keylen, L"agentname")) { config.agentname = Strdup(s); } else if(!Strncmpci(key, keylen, L"nthreads")) { config.nthreads = v; } else if(!Strncmpci(key, keylen, L"dbgfile")) { config.dbgfile = Strdup(s); } else if(!Strncmpci(key, keylen, L"dbg")) { for(i = 0; i < vallen; i++) { c = val[i]; if(c < sizeof(config.dbg)) config.dbg[c]++; else { ok = 0; break; } } } else ok = 0; return ok; }