/* ** Check the Memory Cache (History list) BEFORE filter ** --------------------------------------------------- ** Check if document is already loaded. The user can define whether ** the history list should follow normal expiration or work as a ** traditional history list where expired documents are not updated. ** We don't check for anything but existence proof of a document ** associated with the anchor as the definition is left to the application */ PUBLIC int HTMemoryCacheFilter (HTRequest * request, void * param, int mode) { HTReload validation = HTRequest_reloadMode(request); HTParentAnchor * anchor = HTRequest_anchor(request); void * document = HTAnchor_document(anchor); /* ** We only check the memory cache if it's a GET method */ if (HTRequest_method(request) != METHOD_GET) { HTTRACE(CACHE_TRACE, "Mem Cache... We only check GET methods\n"); return HT_OK; } /* ** If we are asked to flush the persistent cache then there is no reason ** to do anything here - we're flushing it anyway. Also if no document ** then just exit from this filter. */ if (!document || validation > HT_CACHE_FLUSH_MEM) { HTTRACE(CACHE_TRACE, "Mem Cache... No fresh document...\n"); return HT_OK; } /* ** If we have a document object associated with this anchor then we also ** have the object in the history list. Depending on what the user asked, ** we can add a cache validator */ if (document && validation != HT_CACHE_FLUSH_MEM) { HTTRACE(CACHE_TRACE, "Mem Cache... Document already in memory\n"); return HT_LOADED; } return HT_OK; }
PRIVATE int HTSC_putBlock (HTStream * me, const char * b, int l) { me->cur_size += l; /* ** If we get a buffer overflow and we are going to PUT or POST the document ** then ask the user whether it is OK to proceed buffering. Otherwise we ** must give up the request. In all other cases we stop if the buffer fills ** up. */ if (!me->ignore && me->max_size > 0 && me->cur_size > me->max_size) { HTMethod method = HTRequest_method(me->request); if (HTMethod_hasEntity(method)) { HTAlertCallback *cbf = HTAlert_find(HT_A_CONFIRM); if ((cbf && (*cbf)(me->request, HT_A_CONFIRM, HT_MSG_BIG_PUT, NULL, NULL, NULL))) me->ignore = YES; else me->give_up = YES; } else { me->give_up = YES; } } else if (!me->ensure) { HTParentAnchor * anchor = HTRequest_anchor(me->request); int cl = HTAnchor_length(anchor); if (cl > 0) HTChunk_ensure(me->chunk, cl); me->ensure = YES; } if (!me->give_up) { HTChunk_putb(me->chunk, b, l); return HT_OK; } return HT_ERROR; }
/* Add entry to the log file ** ------------------------- ** Format: <HOST> - - <DATE> <METHOD> <URI> <RESULT> <CONTENT_LENTGH> ** which is almost equivalent to Common Logformat. Permissions on UNIX ** are modified by umask. ** ** Returns YES if OK, NO on error ** ** BUG: No result code is produced :-( Should be taken from HTError.c */ PUBLIC BOOL HTLog_addCLF (HTLog * log, HTRequest * request, int status) { if (log && log->fp) { time_t now = time(NULL); HTParentAnchor * anchor = HTRequest_anchor(request); char * uri = HTAnchor_address((HTAnchor *) anchor); HTTRACE(APP_TRACE, "Log......... Writing CLF log\n"); fprintf(log->fp, "localhost - - [%s] %s %s %d %ld\n", HTDateTimeStr(&now, log->localtime), HTMethod_name(HTRequest_method(request)), uri ? uri : "<null>", /* Bill Rizzi */ abs(status), HTAnchor_length(anchor)); HT_FREE(uri); log->accesses++; return (fflush(log->fp) != EOF); /* Actually update it on disk */ } return NO; }
PUBLIC HTStream * HTTPRequest_new (HTRequest * request, HTStream * target, BOOL endHeader, int version) { HTStream * me; if ((me = (HTStream *) HT_CALLOC(1, sizeof(HTStream))) == NULL) HT_OUTOFMEM("HTTPRequest_new"); me->isa = &HTTPRequestClass; me->target = target; me->request = request; me->version = version; me->transparent = NO; /* ** If sending a body in the request then we want a 100 code! */ if (HTMethod_hasEntity(HTRequest_method(request))) HTRequest_addExpect(request, "100-continue", ""); /* Return general HTTP header stream */ return HTTPGen_new(request, me, endHeader, version); }
PRIVATE int pumpData (HTStream * me) { HTRequest * request = me->request; HTResponse * response = me->response; HTFormat format = HTResponse_format(response); HTList * te = HTResponse_transfer(response); HTList * ce = HTResponse_encoding(response); long length = HTResponse_length(response); HTStream * BlackHole = HTBlackHole(); BOOL savestream = NO; me->transparent = YES; /* Pump rest of data right through */ /* ** Cache the metainformation in the anchor object by copying ** it from the response object. This we do regardless if ** we have a persistent cache or not as the memory cache will ** use it as well. If we are updating a cache entry using ** byte ranges then we already have the metainformation and ** hence we can ignore the new one as it'd better be the same. */ if (!(me->mode & HT_MIME_PARTIAL) && HTResponse_isCachable(me->response) != HT_NO_CACHE) HTAnchor_update(HTRequest_anchor(request), me->response); /* ** If we asked only to read the header or footer or we used a HEAD ** method then we stop here as we don't expect any body part. */ if (me->mode & (HT_MIME_HEADER | HT_MIME_FOOTER) || HTRequest_method(request) == METHOD_HEAD) { HTAlertCallback * cbf = HTAlert_find(HT_PROG_DONE); if (cbf) (*cbf)(request, HT_PROG_DONE, HT_MSG_NULL, NULL, NULL, NULL); return HT_LOADED; } /* ** If we are paring a 1xx response then return HT_CONTINUE */ if (me->mode & HT_MIME_CONT) return HT_CONTINUE; /* ** If we get a 101 Protocol Switch then we are done here ** but not done with the response (which we don't know ** how to go about parsing */ if (me->mode & HT_MIME_UPGRADE) { me->hasBody = YES; return HT_OK; } /* ** If there is no content-length, no transfer encoding and no ** content type then we assume that there is no body part in ** the message and we can return HT_LOADED */ { HTHost * host = HTNet_host(me->net); if (length<0 && te==NULL && HTHost_isPersistent(host) && !HTHost_closeNotification(host)) { if (format != WWW_UNKNOWN) { HTTRACE(STREAM_TRACE, "MIME Parser. BAD - there seems to be a body but no length. This must be an HTTP/1.0 server pretending that it is HTTP/1.1\n"); HTHost_setCloseNotification(host, YES); } else { HTAlertCallback * cbf = HTAlert_find(HT_PROG_DONE); if (cbf) (*cbf)(request, HT_PROG_DONE, HT_MSG_NULL, NULL, NULL, NULL); HTTRACE(STREAM_TRACE, "MIME Parser. No body in this message\n"); return HT_LOADED; } } } /* ** Deal with the body */ me->hasBody = YES; /* ** Handle any Content Type */ if (!(me->mode & HT_MIME_PARTIAL) && (format != WWW_UNKNOWN || length > 0 || te)) { HTStream * target; HTTRACE(STREAM_TRACE, "Building.... C-T stack from %s to %s\n" _ HTAtom_name(format) _ HTAtom_name(me->target_format)); if ((target = HTStreamStack(format, me->target_format, me->target, request, YES))==BlackHole) { if (!savestream) { if (me->target) (*me->target->isa->abort)(me->target, NULL); me->target = me->save_stream(request, NULL, format, me->target_format, me->target); savestream = YES; } } else me->target = target; } /* ** Handle any Content Encodings */ HTTRACE(STREAM_TRACE, "Building.... Content-Decoding stack\n"); if (ce) { HTStream * target = HTContentDecodingStack(ce, me->target, request, NULL); if (target == BlackHole) { if (!savestream) { if (me->target) (*me->target->isa->abort)(me->target, NULL); me->target = me->save_stream(request, NULL, format, me->target_format, me->target); savestream = YES; } } else me->target = target; } /* ** Can we cache the data object? If so then create a T stream and hook it ** into the stream pipe. We do it before the transfer decoding so that we ** don't have to deal with that when we retrieve the object from cache. ** If we are appending to a cache entry then use a different stream than ** if creating a new entry. */ #ifndef NO_CACHE if (HTCacheMode_enabled()) { if (me->mode & HT_MIME_PARTIAL) { HTStream * append = HTStreamStack(WWW_CACHE_APPEND, me->target_format, me->target, request, NO); if (append) me->target = HTTee(me->target, append, NULL); #if 0 /* @@ JK: change */ if (append) me->target = append; #endif } else if (HTResponse_isCachable(me->response) == HT_CACHE_ALL) { HTStream * cache = HTStreamStack(WWW_CACHE, me->target_format, me->target, request, NO); if (cache) me->target = HTTee(me->target, cache, NULL); } } #endif /* ** Handle any Transfer Encodings */ HTTRACE(STREAM_TRACE, "Building.... Transfer-Decoding stack\n"); if (te) { HTStream * target = HTTransferDecodingStack(te, me->target, request, NULL); if (target == BlackHole) { if (!savestream) { if (me->target) (*me->target->isa->abort)(me->target, NULL); me->target = me->save_stream(request, NULL, format, me->target_format, me->target); savestream = YES; } } else me->target = target; } /* ** If we for some reason couldn't find a target stream */ if (!me->target) me->target = HTBlackHole(); return HT_OK; }
PRIVATE int FileEvent (SOCKET soc, void * pVoid, HTEventType type) { file_info *file = pVoid; /* Specific access information */ int status = HT_ERROR; HTNet * net = file->net; HTRequest * request = HTNet_request(net); HTParentAnchor * anchor = HTRequest_anchor(request); /* ** Initiate a new file structure and bind to request structure ** This is actually state FILE_BEGIN, but it can't be in the state ** machine as we need the structure first. */ if (type == HTEvent_CLOSE) { /* Interrupted */ HTRequest_addError(request, ERR_FATAL, NO, HTERR_INTERRUPTED, NULL, 0, "HTLoadFile"); FileCleanup(request, HT_INTERRUPTED); return HT_OK; } /* Now jump into the machine. We know the state from the previous run */ while (1) { switch (file->state) { case FS_BEGIN: /* We only support safe (GET, HEAD, etc) methods for the moment */ if (!HTMethod_isSafe(HTRequest_method(request))) { HTRequest_addError(request, ERR_FATAL, NO, HTERR_NOT_ALLOWED, NULL, 0, "HTLoadFile"); file->state = FS_ERROR; break; } /* Check whether we have access to local disk at all */ if (HTLib_secure()) { HTTRACE(PROT_TRACE, "LoadFile.... No access to local file system\n"); file->state = FS_TRY_FTP; break; } file->local = HTWWWToLocal(HTAnchor_physical(anchor), "", HTRequest_userProfile(request)); if (!file->local) { file->state = FS_TRY_FTP; break; } /* Create a new host object and link it to the net object */ { HTHost * host = NULL; if ((host = HTHost_new("localhost", 0)) == NULL) return HT_ERROR; HTNet_setHost(net, host); if (HTHost_addNet(host, net) == HT_PENDING) { HTTRACE(PROT_TRACE, "HTLoadFile.. Pending...\n"); /* move to the hack state */ file->state = FS_PENDING; return HT_OK; } } file->state = FS_DO_CN; break; case FS_PENDING: /* ** 2000/08/10 JK : This is a funny state. Because of the ** internal libwww stacks, when doing multiple local ** requests (e.g., while using the Robot), we need to ask ** again for the host object. If we had jumped directly to ** the FS_DO_CN state, libwww would have blocked because ** of socket starvation. ** This state is similar to FS_BEGINNING, but just requests ** the host object. ** YES. THIS IS AN UGLY HACK!! */ { HTHost * host = NULL; if ((host = HTHost_new("localhost", 0)) == NULL) return HT_ERROR; HTNet_setHost(net, host); if (HTHost_addNet(host, net) == HT_PENDING) { HTTRACE(PROT_TRACE, "HTLoadFile.. Pending...\n"); file->state = FS_PENDING; return HT_OK; } } file->state = FS_DO_CN; break; case FS_DO_CN: /* ** If we have to do content negotiation then find the object that ** fits best into either what the client has indicated in the ** accept headers or what the client has registered on its own. ** The object chosen can in fact be a directory! However, content ** negotiation only makes sense if we can read the directory! ** We stat the file in order to find the size and to see it if ** exists. */ if (HTRequest_negotiation(request) && HTMethod_isSafe(HTRequest_method(request))) { char * conneg = HTMulti(request, file->local,&file->stat_info); if (conneg) { HT_FREE(file->local); file->local = conneg; HTAnchor_setPhysical(anchor, conneg); HTTRACE(PROT_TRACE, "Load File... Found `%s\'\n" _ conneg); } else { HTTRACE(PROT_TRACE, "Load File... Not found - even tried content negotiation\n"); HTRequest_addError(request, ERR_FATAL, NO, HTERR_NOT_FOUND, NULL, 0, "HTLoadFile"); file->state = FS_ERROR; break; } } else { if (HT_STAT(file->local, &file->stat_info) == -1) { HTTRACE(PROT_TRACE, "Load File... Not found `%s\'\n" _ file->local); HTRequest_addError(request, ERR_FATAL, NO, HTERR_NOT_FOUND, NULL, 0, "HTLoadFile"); file->state = FS_ERROR; break; } } /* ** Check to see if the 'localname' is in fact a directory. ** Note that we can't do a HEAD on a directory */ if (((file->stat_info.st_mode) & S_IFMT) == S_IFDIR) { if (HTRequest_method(request) == METHOD_GET) file->state = FS_PARSE_DIR; else { HTRequest_addError(request, ERR_INFO, NO, HTERR_NO_CONTENT, NULL, 0, "HTLoadFile"); file->state = FS_NO_DATA; } break; } /* ** If empty file then only serve it if it is editable. We also get ** the bindings for the file suffixes in lack of better bindings */ { BOOL editable = HTEditable(file->local, &file->stat_info); if (file_suffix_binding) HTBind_getAnchorBindings(anchor); if (editable) HTAnchor_appendAllow(anchor, METHOD_PUT); /* Set the file size */ if (file->stat_info.st_size) HTAnchor_setLength(anchor, file->stat_info.st_size); /* Set the file last modified time stamp */ if (file->stat_info.st_mtime > 0) HTAnchor_setLastModified(anchor, file->stat_info.st_mtime); /* Check to see if we can edit it */ if (!editable && !file->stat_info.st_size) { HTRequest_addError(request, ERR_INFO, NO, HTERR_NO_CONTENT, NULL, 0, "HTLoadFile"); file->state = FS_NO_DATA; } else { file->state = (HTRequest_method(request)==METHOD_GET) ? FS_NEED_OPEN_FILE : FS_GOT_DATA; } } break; case FS_NEED_OPEN_FILE: status = HTFileOpen(net, file->local, HT_FB_RDONLY); if (status == HT_OK) { /* ** Create the stream pipe FROM the channel to the application. ** The target for the input stream pipe is set up using the ** stream stack. */ { HTStream * rstream = HTStreamStack(HTAnchor_format(anchor), HTRequest_outputFormat(request), HTRequest_outputStream(request), request, YES); HTNet_setReadStream(net, rstream); HTRequest_setOutputConnected(request, YES); } /* ** Create the stream pipe TO the channel from the application ** and hook it up to the request object */ { HTOutputStream * output = HTNet_getOutput(net, NULL, 0); HTRequest_setInputStream(request, (HTStream *) output); } /* ** Set up concurrent read/write if this request isn't the ** source for a PUT or POST. As source we don't start reading ** before all destinations are ready. If destination then ** register the input stream and get ready for read */ if (HTRequest_isSource(request) && !HTRequest_destinationsReady(request)) return HT_OK; HTRequest_addError(request, ERR_INFO, NO, HTERR_OK, NULL, 0, "HTLoadFile"); file->state = FS_NEED_BODY; /* If we are _not_ using preemptive mode and we are Unix fd's ** then return here to get the same effect as when we are ** connecting to a socket. That way, HTFile acts just like any ** other protocol module even though we are in fact doing ** blocking connect */ if (HTEvent_isCallbacksRegistered()) { if (!HTRequest_preemptive(request)) { if (!HTNet_preemptive(net)) { HTTRACE(PROT_TRACE, "HTLoadFile.. Returning\n"); HTHost_register(HTNet_host(net), net, HTEvent_READ); } else if (!file->timer) { HTTRACE(PROT_TRACE, "HTLoadFile.. Returning\n"); file->timer = HTTimer_new(NULL, ReturnEvent, file, 1, YES, NO); } return HT_OK; } } } else if (status == HT_WOULD_BLOCK || status == HT_PENDING) return HT_OK; else { HTRequest_addError(request, ERR_INFO, NO, HTERR_INTERNAL, NULL, 0, "HTLoadFile"); file->state = FS_ERROR; /* Error or interrupt */ } break; case FS_NEED_BODY: status = HTHost_read(HTNet_host(net), net); if (status == HT_WOULD_BLOCK) return HT_OK; else if (status == HT_LOADED || status == HT_CLOSED) { file->state = FS_GOT_DATA; } else { HTRequest_addError(request, ERR_INFO, NO, HTERR_FORBIDDEN, NULL, 0, "HTLoadFile"); file->state = FS_ERROR; } break; case FS_PARSE_DIR: status = HTFile_readDir(request, file); if (status == HT_LOADED) file->state = FS_GOT_DATA; else file->state = FS_ERROR; break; case FS_TRY_FTP: { char *url = HTAnchor_physical(anchor); HTAnchor *anchor; char *newname = NULL; StrAllocCopy(newname, "ftp:"); if (!strncmp(url, "file:", 5)) StrAllocCat(newname, url+5); else StrAllocCat(newname, url); anchor = HTAnchor_findAddress(newname); HTRequest_setAnchor(request, anchor); HT_FREE(newname); FileCleanup(request, HT_IGNORE); return HTLoad(request, YES); } break; case FS_GOT_DATA: FileCleanup(request, HT_LOADED); return HT_OK; break; case FS_NO_DATA: FileCleanup(request, HT_NO_DATA); return HT_OK; break; case FS_RETRY: FileCleanup(request, HT_RETRY); return HT_OK; break; case FS_ERROR: FileCleanup(request, HT_ERROR); return HT_OK; break; } } /* End of while(1) */ }
PRIVATE int NewsEvent (SOCKET soc, void * pVoid, HTEventType type) { news_info *news = (news_info *)pVoid; int status = HT_ERROR; HTNet * net = news->net; HTRequest * request = HTNet_request(net); HTParentAnchor * anchor = HTRequest_anchor(request); char * url = HTAnchor_physical(anchor); HTHost * host = HTNet_host(net); /* ** Initiate a new nntp structure and bind to request structure ** This is actually state NNTP_BEGIN, but it can't be in the state ** machine as we need the structure first. */ if (type == HTEvent_CLOSE) { /* Interrupted */ HTRequest_addError(request, ERR_FATAL, NO, HTERR_INTERRUPTED, NULL, 0, "HTLoadHTTP"); HTNewsCleanup(request, HT_INTERRUPTED); return HT_OK; } else news = (news_info *) HTNet_context(net); /* Get existing copy */ /* Now jump into the machine. We know the state from the previous run */ while (1) { switch (news->state) { case NEWS_BEGIN: news->state = (!strchr(url, '@') && strchr(url, '*')) ? NEWS_SEEK_CACHE : NEWS_NEED_CONNECTION; break; case NEWS_SEEK_CACHE: if (HTNewsCache_before(request, NULL, 0) == HT_LOADED) news->state = NEWS_SUCCESS; else news->state = NEWS_NEED_CONNECTION; break; case NEWS_NEED_CONNECTION: /* Let's set up a connection */ if (!strncasecomp(url, "news:", 5)) { HTUserProfile * up = HTRequest_userProfile(request); char * newshost = HTUserProfile_news(up); StrAllocCopy(news->name, url+5); if (newshost) { char *newshack = NULL; /* Then we can use HTParse :-) */ StrAllocCopy(newshack, "news://"); StrAllocCat(newshack, newshost); status = HTHost_connect(host, net, (char *) newshack); host = HTNet_host(net); HT_FREE(newshack); } else news->state = NEWS_ERROR; } else if (!strncasecomp(url, "nntp:", 5)) { news->name = HTParse(url, "", PARSE_PATH); status = HTHost_connect(host, net, url); host = HTNet_host(net); } else { HTTRACE(PROT_TRACE, "News........ Huh?"); news->state = NEWS_ERROR; } if (status == HT_OK) { BOOL greeting = NO; /* Set up the persistent connection */ if (!HTNet_persistent(net)) { HTNet_setPersistent(net, YES, HT_TP_SINGLE); greeting = YES; } /* ** Check the protocol class to see if we have connected to a ** the right class of server, in this case HTTP. */ { HTHost * host = HTNet_host(net); char * s_class = HTHost_class(host); if (s_class && strcasecomp(s_class, "nntp")) { HTRequest_addError(request, ERR_FATAL, NO, HTERR_CLASS, NULL, 0, "HTLoadNews"); news->state = NEWS_ERROR; break; } HTHost_setClass(host, "nntp"); } /* ** Create the stream pipe FROM the channel to the application. ** The target for the input stream pipe is set up using the ** stream stack. */ { HTStream * rstream = HTNewsStatus_new(request, news, host); HTNet_setReadStream(net, rstream); HTRequest_setOutputConnected(request, YES); } /* ** Create the stream pipe TO the channel from the application ** and hook it up to the request object */ { HTOutputStream * output = HTNet_getOutput(net, NULL, 0); HTRequest_setInputStream(request, (HTStream *) output); } news->state = greeting ? NEWS_NEED_GREETING : NEWS_NEED_SWITCH; } else if (status == HT_WOULD_BLOCK || status == HT_PENDING) return HT_OK; else news->state = NEWS_ERROR; break; case NEWS_NEED_GREETING: status = HTHost_read(HTNet_host(net), net); if (status == HT_WOULD_BLOCK) return HT_OK; else if (status == HT_LOADED) { if (news->repcode/100 == 2) news->state = NEWS_NEED_SWITCH; else news->state = NEWS_ERROR; } else news->state = NEWS_ERROR; break; case NEWS_NEED_SWITCH: { HTMethod method = HTRequest_method(request); /* ** Find out what to ask the news server. Syntax of address is ** xxx@yyy Article ** <xxx@yyy> Same article ** xxxxx News group (no "@") */ if (method == METHOD_GET) { if (strchr(url, '@')) { /* ARTICLE */ if (*(news->name) != '<') { /* Add '<' and '>' */ char *newart; if ((newart = (char *) HT_MALLOC(strlen(news->name)+3)) == NULL) HT_OUTOFMEM("HTLoadNews"); sprintf(newart, "<%s>", news->name); HT_FREE(news->name); news->name = newart; } news->state = NEWS_NEED_ARTICLE; } else if (strchr(url, '*')) news->state = NEWS_NEED_LIST; else news->state = NEWS_NEED_GROUP; } else if (method == METHOD_POST) news->state = NEWS_NEED_POST; else { HTRequest_addError(request, ERR_FATAL, NO, HTERR_NOT_IMPLEMENTED,NULL, 0,"HTLoadNews"); news->state = NEWS_ERROR; } HTUnEscape(news->name); HTCleanTelnetString(news->name); } break; case NEWS_NEED_ARTICLE: if (!news->sent) { status = SendCommand(request, news, "ARTICLE", news->name); if (status == HT_WOULD_BLOCK) return HT_OK; else if (status == HT_ERROR) news->state = NEWS_ERROR; news->format = WWW_MIME; /* ** Set the default content type to plain text as news servers ** almost never send any useful information about the length ** of the body or the type - the success of MIME! */ HTAnchor_setFormat(anchor, WWW_PLAINTEXT); news->sent = YES; } else { status = HTHost_read(HTNet_host(net), net); if (status == HT_WOULD_BLOCK) return HT_OK; else if (status == HT_OK) news->state = NEWS_NEED_BODY; else if (status == HT_LOADED) { news->state = (news->repcode/100 == 2) ? NEWS_SUCCESS : NEWS_ERROR; } else news->state = NEWS_ERROR; news->sent = NO; } break; #if HT_LISTGROUP case NEWS_NEED_LGRP: if (!news->sent) { status = SendCommand(request, news, "LIST", "NEWSGROUPS"); if (status == HT_WOULD_BLOCK) return HT_OK; else if (status == HT_ERROR) news->state = NEWS_ERROR; news->format = WWW_NNTP_LIST; news->sent = YES; } else { status = HTHost_read(HTNet_host(net), net); if (status == HT_WOULD_BLOCK) return HT_OK; else if (status == HT_OK) news->state = NEWS_NEED_BODY; else if (status == HT_LOADED) { news->state = (news->repcode/100 == 2) ? NEWS_SUCCESS : NEWS_NEED_LIST; } else news->state = NEWS_ERROR; news->sent = NO; } break; #endif /* HT_LISTGROUP */ case NEWS_NEED_LIST: if (!news->sent) { status = SendCommand(request, news, "LIST", NULL); if (status == HT_WOULD_BLOCK) return HT_OK; else if (status == HT_ERROR) news->state = NEWS_ERROR; news->format = WWW_NNTP_LIST; news->sent = YES; } else { status = HTHost_read(HTNet_host(net), net); if (status == HT_WOULD_BLOCK) return HT_OK; else if (status == HT_OK) news->state = NEWS_NEED_BODY; else if (status == HT_LOADED) { news->state = (news->repcode/100 == 2) ? NEWS_SUCCESS : NEWS_ERROR; } else news->state = NEWS_ERROR; news->sent = NO; } break; case NEWS_NEED_GROUP: if (!news->sent) { status = SendCommand(request, news, "GROUP", news->name); if (status == HT_WOULD_BLOCK) return HT_OK; else if (status == HT_ERROR) news->state = NEWS_ERROR; news->sent = YES; } else { status = HTHost_read(HTNet_host(net), net); if (status == HT_WOULD_BLOCK) return HT_OK; else if (status == HT_LOADED) { if (news->repcode/100 == 2) { if (sscanf(news->reply, "%d%d%d", &news->total, &news->first, &news->last) == 3) { if (MaxArt && news->total>MaxArt) news->last = news->first-MaxArt; news->current = news->first; /* If no content in this group */ if (news->first == news->last) { HTRequest_addError(request, ERR_FATAL, NO, HTERR_NO_CONTENT, NULL, 0, "HTLoadNews"); news->state = NEWS_NO_DATA; break; } news->state = NEWS_NEED_XOVER; } else news->state = NEWS_ERROR; } else news->state = NEWS_ERROR; } else news->state = NEWS_ERROR; news->sent = NO; } break; case NEWS_NEED_XOVER: if (!news->sent) { char buf[20]; sprintf(buf, "%d-%d", news->first, news->last); status = SendCommand(request, news, "XOVER", buf); if (status == HT_WOULD_BLOCK) return HT_OK; else if (status == HT_ERROR) news->state = NEWS_ERROR; news->format = WWW_NNTP_OVER; news->sent = YES; } else { status = HTHost_read(HTNet_host(net), net); if (status == HT_WOULD_BLOCK) return HT_OK; else if (status == HT_OK) news->state = NEWS_NEED_BODY; else if (status == HT_LOADED) { if (news->repcode/100 == 2) news->state = NEWS_SUCCESS; else { news->format = WWW_NNTP_HEAD; news->state = NEWS_NEED_HEAD; } } else news->state = NEWS_ERROR; news->sent = NO; } break; case NEWS_NEED_HEAD: if (!news->sent) { char buf[10]; sprintf(buf, "%d", news->current++); status = SendCommand(request, news, "HEAD", buf); if (status == HT_WOULD_BLOCK) return HT_OK; else if (status == HT_ERROR) news->state = NEWS_ERROR; news->sent = YES; } else { status = HTHost_read(HTNet_host(net), net); if (status == HT_WOULD_BLOCK) return HT_OK; else if (status == HT_LOADED) { if (news->repcode/100 == 2) { if (news->current > news->last) news->state = NEWS_SUCCESS; } else news->state = NEWS_ERROR; } else news->state = NEWS_ERROR; news->sent = NO; } break; case NEWS_NEED_POST: { HTStream * oldinput = HTRequest_inputStream(request); HTStream * newinput = HTNewsPost_new(request, HTBuffer_new(oldinput, request,512)); HTRequest_setInputStream(request, newinput); /* Remember to convert to CRLF */ } news->state = NEWS_NEED_BODY; break; case NEWS_NEED_BODY: if (type == HTEvent_WRITE || type == HTEvent_BEGIN) { if (HTRequest_isDestination(request)) { HTRequest * source = HTRequest_source(request); HTNet * srcnet = HTRequest_net(source); if (srcnet) { HTHost_register(HTNet_host(srcnet), srcnet, HTEvent_READ); HTHost_unregister(HTNet_host(srcnet), srcnet, HTEvent_WRITE); } return HT_OK; } /* ** Should we use the input stream directly or call the post ** callback function to send data down to the network? */ { HTStream * input = HTRequest_inputStream(request); HTPostCallback * pcbf = HTRequest_postCallback(request); if (pcbf) { status = pcbf(request, input); if (status == HT_PAUSE || status == HT_LOADED) type = HTEvent_READ; } else { status = (*input->isa->flush)(input); type = HTEvent_READ; } if (status == HT_WOULD_BLOCK) return HT_OK; } status = request->PostCallback ? request->PostCallback(request, request->input_stream) : (*request->input_stream->isa->flush)(request->input_stream); if (status == HT_WOULD_BLOCK) return HT_OK; else type = HTEvent_READ; /* Trick to ensure that we do READ */ } else if (type == HTEvent_READ) { status = HTHost_read(HTNet_host(net), net); if (status == HT_WOULD_BLOCK) return HT_OK; else if (status == HT_LOADED) news->state = NEWS_SUCCESS; else news->state = NEWS_ERROR; } else { news->state = NEWS_ERROR; } break; case NEWS_SUCCESS: HTNewsCleanup(request, HT_LOADED); return HT_OK; break; case NEWS_NO_DATA: HTNewsCleanup(request, HT_NO_DATA); return HT_OK; break; case NEWS_ERROR: HTNewsCleanup(request, HT_NOT_FOUND); return HT_OK; break; } } /* End of while(1) */ }
/* ** Redirection AFTER filter ** ------------------------ ** The redirection handler only handles redirections ** on the GET or HEAD method (or any other safe method) */ PUBLIC int HTRedirectFilter (HTRequest * request, HTResponse * response, void * param, int status) { HTMethod method = HTRequest_method(request); HTAnchor * new_anchor = HTResponse_redirection(response); /* Check for destination */ if (!new_anchor) { HTTRACE(PROT_TRACE, "Redirection. No destination\n"); return HT_OK; } /* ** Only do automatic redirect on GET and HEAD. Ask for all ** other methods. */ if (!HTMethod_isSafe(method)) { /* ** If we got a 303 See Other then change the method to GET. ** Otherwise ask the user whether we should continue. */ if (status == HT_SEE_OTHER) { HTTRACE(PROT_TRACE, "Redirection. Changing method from %s to GET\n" _ HTMethod_name(method)); HTRequest_setMethod(request, METHOD_GET); } else { HTAlertCallback * prompt = HTAlert_find(HT_A_CONFIRM); if (prompt) { if ((*prompt)(request, HT_A_CONFIRM, HT_MSG_REDIRECTION, NULL, NULL, NULL) != YES) return HT_OK; } } } /* Register the redirection as a link relationship */ { HTLinkType ltype = status==HT_PERM_REDIRECT ? HT_LR_PERM_REDIRECT : (status==HT_TEMP_REDIRECT || status==HT_FOUND) ? HT_LR_TEMP_REDIRECT : status==HT_SEE_OTHER ? HT_LR_SEE_OTHER : NULL; if (ltype) { HTLink_add((HTAnchor *) HTRequest_anchor(request), new_anchor, ltype, method); } } /* Delete any auth credendials as they get regenerated */ HTRequest_deleteCredentialsAll(request); /* ** Start new request with the redirect anchor found in the headers. ** Note that we reuse the same request object which means that we must ** keep this around until the redirected request has terminated. It also ** allows us in an easy way to keep track of the number of redirections ** so that we can detect endless loops. */ if (HTRequest_doRetry(request)) { HTLoadAnchor(new_anchor, request); } else { HTRequest_addError(request, ERR_FATAL, NO, HTERR_MAX_REDIRECT, NULL, 0, "HTRedirectFilter"); return HT_OK; /* Wanna fall through */ } /* ** By returning HT_ERROR we make sure that this is the last handler to be ** called. We do this as we don't want any other filter to delete the ** request object now when we have just started a new one ourselves */ return HT_ERROR; }
/* HTTPMakeRequest ** --------------- ** Makes a HTTP/1.0-1.1 request header. */ PRIVATE int HTTPMakeRequest (HTStream * me, HTRequest * request) { HTMethod method = HTRequest_method(request); HTRqHd request_mask = HTRequest_rqHd(request); HTParentAnchor * anchor = HTRequest_anchor(request); char * etag = HTAnchor_etag(anchor); char crlf[3]; char qstr[10]; *crlf = CR; *(crlf+1) = LF; *(crlf+2) = '\0'; /* Generate the HTTP/1.x RequestLine */ if (me->state == 0) { if (method != METHOD_INVALID) { PUTS(HTMethod_name(method)); PUTC(' '); } else PUTS("GET "); me->state++; } /* ** Generate the Request URI. If we are using full request URI then it's ** easy. Otherwise we must filter out the path part of the URI. ** In case it's a OPTIONS request then if there is no pathinfo then use ** a * instead. If we use a method different from GET or HEAD then use ** the content-location if available. */ if (me->state == 1) { char * abs_location = NULL; char * addr = HTAnchor_physical(anchor); char * location; /* JK: If the application specified a content-location (which is stored in the request in default put-name!), we use it instead of the URL that's being saved to. This is like having a user defined Content-Location */ location = HTRequest_defaultPutName (request); if (location) { if (HTURL_isAbsolute (location)) { char * relative; relative = HTRelative (location, location); abs_location = HTParse (relative + 2, addr, PARSE_ALL); HT_FREE (relative); } else abs_location = HTParse (location, addr, PARSE_ALL); addr = abs_location; } #if 0 /* ** We don't use the content-location any more as it is superseeded ** by etags and the combination of the two might do more harm than ** good (The etag is not guaranteed to be unique over multiple URIs) */ /* ** If we are using a method different from HEAD and GET then use ** the Content-Location if available, else the Request-URI. */ if (!HTMethod_isSafe(method)) { char * location = HTAnchor_location(anchor); if (location) { if (HTURL_isAbsolute(location)) addr = location; else { /* ** We have a content-location but it is relative and ** must expand it either to the content-base or to ** the Request-URI itself. */ char * base = HTAnchor_base(anchor); abs_location = HTParse(location, base, PARSE_ALL); addr = abs_location; } } } #endif /* ** If we are using a proxy or newer versions of HTTP then we can ** send the full URL. Otherwise we only send the path. */ if (HTRequest_fullURI(request)) StrAllocCopy(me->url, addr); else { me->url = HTParse(addr, "", PARSE_PATH | PARSE_PUNCTUATION); if (method == METHOD_OPTIONS) { /* ** We don't preserve the final slash or lack of same through ** out the code. This is mainly for optimization reasons ** but it gives a problem OPTIONS. We can either send a "*" ** or a "/" but not both. For now we send a "*". */ if (!strcmp(me->url, "/")) *me->url = '*'; } } HT_FREE(abs_location); me->state++; } /* ** Now send the URL that we have put together */ if (me->state == 2) { int status = HT_OK; if ((status = PUTS(me->url)) != HT_OK) return status; me->state++; #if 0 fprintf(stderr, "Requesting '%s'\n", me->url); #endif } PUTC(' '); /* ** Send out the version number. If we know it is a HTTP/1.0 server we ** are talking to then use HTTP/1.0, else use HTTP/1.1 as default version ** number */ if (me->version == HTTP_10) PUTS(HTTP_VERSION_10); else PUTS(HTTP_VERSION); PUTBLOCK(crlf, 2); /* Request Headers */ if (request_mask & HT_C_ACCEPT_TYPE) { HTFormat format = HTRequest_outputFormat(request); /* ** If caller has specified a specific output format then use this. ** Otherwise use all the registered converters to generate the ** accept header */ if (format == WWW_PRESENT) { int list; HTList *cur; BOOL first=YES; for (list=0; list<2; list++) { if ((!list && ((cur = HTFormat_conversion()) != NULL)) || (list && ((cur = HTRequest_conversion(request))!=NULL))) { HTPresentation * pres; while ((pres=(HTPresentation *) HTList_nextObject(cur))) { if (pres->rep_out==WWW_PRESENT && pres->quality<=1.0) { if (first) { PUTS("Accept: "); first=NO; } else PUTC(','); PUTS(HTAtom_name(pres->rep)); if (pres->quality < 1.0 && pres->quality >= 0.0) { sprintf(qstr, ";q=%1.1f", pres->quality); PUTS(qstr); } } } } } if (!first) PUTBLOCK(crlf, 2); } else { /* ** If we have an explicit output format then only send ** this one if not this is an internal libwww format ** of type www/<star> */ if (!HTMIMEMatch(WWW_INTERNAL, format)) { PUTS("Accept: "); PUTS(HTAtom_name(format)); PUTBLOCK(crlf, 2); } } } if (request_mask & HT_C_ACCEPT_CHAR) { int list; HTList *cur; BOOL first=YES; for (list=0; list<2; list++) { if ((!list && ((cur = HTFormat_charset()) != NULL)) || (list && ((cur = HTRequest_charset(request)) != NULL))) { HTAcceptNode *pres; while ((pres = (HTAcceptNode *) HTList_nextObject(cur))) { if (first) { PUTS("Accept-Charset: "); first=NO; } else PUTC(','); PUTS(HTAtom_name(pres->atom)); if (pres->quality < 1.0 && pres->quality >= 0.0) { sprintf(qstr, ";q=%1.1f", pres->quality); PUTS(qstr); } } } } if (!first) PUTBLOCK(crlf, 2); } if (request_mask & HT_C_ACCEPT_ENC) { int list; HTList *cur; BOOL first=YES; for (list=0; list<2; list++) { if ((!list && ((cur = HTFormat_contentCoding()) != NULL)) || (list && ((cur = HTRequest_encoding(request)) != NULL))) { HTCoding * pres; while ((pres = (HTCoding *) HTList_nextObject(cur))) { double quality = HTCoding_quality(pres); if (first) { PUTS("Accept-Encoding: "); first = NO; } else PUTC(','); PUTS(HTCoding_name(pres)); if (quality < 1.0 && quality >= 0.0) { sprintf(qstr, ";q=%1.1f", quality); PUTS(qstr); } } } } if (!first) PUTBLOCK(crlf, 2); } if (request_mask & HT_C_ACCEPT_TE) { int list; HTList *cur; BOOL first=YES; for (list=0; list<2; list++) { if ((!list && ((cur = HTFormat_transferCoding()) != NULL)) || (list && ((cur = HTRequest_transfer(request)) != NULL))) { HTCoding * pres; while ((pres = (HTCoding *) HTList_nextObject(cur))) { double quality = HTCoding_quality(pres); const char * coding = HTCoding_name(pres); if (first) { PUTS("TE: "); first = NO; } else PUTC(','); /* Special check for "chunked" which is translated to "trailers" */ if (!strcasecomp(coding, "chunked")) PUTS("trailers"); else PUTS(coding); if (quality < 1.0 && quality >= 0.0) { sprintf(qstr, ";q=%1.1f", quality); PUTS(qstr); } } } } if (!first) PUTBLOCK(crlf, 2); } if (request_mask & HT_C_ACCEPT_LAN) { int list; HTList *cur; BOOL first=YES; for (list=0; list<2; list++) { if ((!list && ((cur = HTFormat_language()) != NULL)) || (list && ((cur = HTRequest_language(request)) != NULL))) { HTAcceptNode *pres; while ((pres = (HTAcceptNode *) HTList_nextObject(cur))) { if (first) { PUTS("Accept-Language: "); first=NO; } else PUTC(','); PUTS(HTAtom_name(pres->atom)); if (pres->quality < 1.0 && pres->quality >= 0.0) { sprintf(qstr, ";q=%1.1f", pres->quality); PUTS(qstr); } } } } if (!first) PUTBLOCK(crlf, 2); } if (request_mask & HT_C_AUTH) { HTAssocList * cur = HTRequest_credentials(request); if (cur) { /* Access authentication */ HTAssoc * pres; while ((pres = (HTAssoc *) HTAssocList_nextObject(cur))) { PUTS(HTAssoc_name(pres)); PUTS(": "); PUTS(HTAssoc_value(pres)); PUTBLOCK(crlf, 2); } } } if (request_mask & HT_C_EXPECT) { HTAssocList * cur = HTRequest_expect(request); if (cur) { BOOL first=YES; HTAssoc * pres; while ((pres = (HTAssoc *) HTAssocList_nextObject(cur))) { char * value = HTAssoc_value(pres); if (first) { PUTS("Expect: "); first = NO; } else PUTC(','); /* Output the name */ PUTS(HTAssoc_name(pres)); /* Only output the value if not empty string */ if (*value) { PUTS("="); PUTS(value); } } PUTBLOCK(crlf, 2); } } if (request_mask & HT_C_FROM) { HTUserProfile * up = HTRequest_userProfile(request); const char * mailaddress = HTUserProfile_email(up); if (mailaddress) { PUTS("From: "); PUTS(mailaddress); PUTBLOCK(crlf, 2); } } if (request_mask & HT_C_HOST) { char *orig = HTAnchor_address((HTAnchor *) anchor); char *host = HTParse(orig, "", PARSE_HOST); char hostace[256]; #if 0 /* Keep the port number for HTTP/1.1 compliance */ char *ptr = strchr(host, ':'); /* Chop off port number */ if (ptr) *ptr = '\0'; #endif PUTS("Host: "); /****** still have to check UTF8toACE with port number */ if (!HTACEfromUTF8 (host, hostace, 255)) { PUTS(hostace); } else { PUTS(host); /* this may be dangerous, but helps server side debugging */ HTTRACE(PROT_TRACE, "HTTP........ Error: Cannot convert to ACE: `%s\'\n" _ host); } PUTBLOCK(crlf, 2); HT_FREE(orig); HT_FREE(host); } /* ** In the "If-*" series of headers, the ones related to etags have higher ** priority than the date relates ones. That is, if we have a etag then ** use that, otherwise use the date. First we check for range, match, and ** unmodified-since. */ if (request_mask & HT_C_IF_RANGE && etag) { PUTS("If-Range: \""); PUTS(etag); PUTC('"'); PUTBLOCK(crlf, 2); HTTRACE(PROT_TRACE, "HTTP........ If-Range using etag `%s\'\n" _ etag); } else if (request_mask & HT_C_IF_MATCH_ANY) { PUTS("If-Match: *"); PUTBLOCK(crlf, 2); HTTRACE(PROT_TRACE, "HTTP........ If-Match using `*\'\n"); } else if (request_mask & HT_C_IF_MATCH && etag) { PUTS("If-Match: \""); PUTS(etag); PUTC('"'); PUTBLOCK(crlf, 2); HTTRACE(PROT_TRACE, "HTTP........ If-Match using etag `%s\'\n" _ etag); } else if (request_mask & HT_C_IF_UNMOD_SINCE) { time_t lm = HTAnchor_lastModified(anchor); if (lm > 0) { PUTS("If-Unmodified-Since: "); PUTS(HTDateTimeStr(&lm, NO)); PUTBLOCK(crlf, 2); HTTRACE(PROT_TRACE, "HTTP........ If-Unmodified-Since `%s\'\n" _ HTDateTimeStr(&lm, NO)); } } /* ** If-None-Match and If-Modified-Since are equivalent except that the ** first uses etags and the second uses dates. Etags have precedence over ** dates. */ if (request_mask & HT_C_IF_NONE_MATCH_ANY) { PUTS("If-None-Match: *"); PUTBLOCK(crlf, 2); HTTRACE(PROT_TRACE, "HTTP........ If-None-Match using `*\'\n"); } else if (request_mask & HT_C_IF_NONE_MATCH && etag) { PUTS("If-None-Match: \""); PUTS(etag); PUTC('"'); PUTBLOCK(crlf, 2); HTTRACE(PROT_TRACE, "HTTP........ If-None-Match `%s\'\n" _ etag); } if (request_mask & HT_C_IMS) { time_t lm = HTAnchor_lastModified(anchor); if (lm > 0) { PUTS("If-Modified-Since: "); PUTS(HTDateTimeStr(&lm, NO)); PUTBLOCK(crlf, 2); HTTRACE(PROT_TRACE, "HTTP........ If-Modified-Since `%s\'\n" _ HTDateTimeStr(&lm, NO)); } } /* ** Max forwards is mainly for TRACE where we want to be able to stop the ** TRACE at a specific location un the message path. */ if (request_mask & HT_C_MAX_FORWARDS) { int hops = HTRequest_maxForwards(request); if (hops >= 0) { sprintf(qstr, "%d", hops); PUTS("Max-Forwards: "); PUTS(qstr); PUTBLOCK(crlf, 2); } } /* ** Range requests. For now, we only take the first entry registered for ** this request. This means that you can only send a single "unit" and ** then a set of range within this unit. This is in accordance with ** HTTP/1.1. Multiple units will go on multiple lines. */ if (request_mask & HT_C_RANGE) { HTAssocList * cur = HTRequest_range(request); if (cur) { /* Range requests */ HTAssoc * pres; while ((pres = (HTAssoc *) HTAssocList_nextObject(cur))) { PUTS("Range: "); PUTS(HTAssoc_name(pres)); /* Unit */ PUTS("="); PUTS(HTAssoc_value(pres)); /* Ranges within this unit */ PUTBLOCK(crlf, 2); } } } if (request_mask & HT_C_REFERER) { HTParentAnchor * parent_anchor = HTRequest_parent(request); if (parent_anchor) { char * act = HTAnchor_address((HTAnchor *) anchor); char * parent = HTAnchor_address((HTAnchor *) parent_anchor); #if 1 char * relative = HTRelative(parent, act); #else char * relative = HTParse(parent, act, PARSE_ACCESS|PARSE_HOST|PARSE_PATH|PARSE_PUNCTUATION); #endif if (relative && *relative) { PUTS("Referer: "); PUTS(relative); PUTBLOCK(crlf, 2); } HT_FREE(act); HT_FREE(parent); HT_FREE(relative); } } if (request_mask & HT_C_USER_AGENT) { PUTS("User-Agent: "); PUTS(HTLib_appName()); PUTC('/'); PUTS(HTLib_appVersion()); PUTC(' '); PUTS(HTLib_name()); PUTC('/'); PUTS(HTLib_version()); PUTBLOCK(crlf, 2); } HTTRACE(PROT_TRACE, "HTTP........ Generating HTTP/1.x Request Headers\n"); return HT_OK; }
PUBLIC char * HTDialog_progressMessage (HTRequest * request, HTAlertOpcode op, int msgnum, const char * dfault, void * input) { char * result = NULL; switch (op) { case HT_PROG_DNS: StrAllocMCopy(&result, "Looking up ", input ? (char *) input : "", NULL); break; case HT_PROG_CONNECT: StrAllocMCopy(&result, "Contacting ", input ? (char *) input : "", NULL); break; case HT_PROG_ACCEPT: StrAllocCopy(result, "Waiting for connection..."); break; case HT_PROG_LOGIN: StrAllocCopy(result, "Logging in..."); break; case HT_PROG_READ: if (request) { long cl = HTAnchor_length(HTRequest_anchor(request)); if (cl > 0) { long b_read = HTRequest_bodyRead(request); double pro = (double) b_read/cl*100; char buf[10]; char pct[10]; HTNumToStr((unsigned long) cl, buf, 10); sprintf(pct, "%d%%", (int) pro); StrAllocMCopy(&result, "Read (", pct, " of ", buf, ")", NULL); } else { long b_read = HTRequest_bytesRead(request); int * raw_read = input ? (int *) input : NULL; if (b_read > 0) { char buf[10]; HTNumToStr(b_read, buf, 10); StrAllocMCopy(&result, "Read ", buf, "bytes", NULL); } else if (raw_read && *raw_read>0) { char buf[10]; HTNumToStr(*raw_read, buf, 10); StrAllocMCopy(&result, "Read ", buf, "bytes", NULL); } else { StrAllocCopy(result, "Reading..."); } } } break; case HT_PROG_WRITE: if (request && HTMethod_hasEntity(HTRequest_method(request))) { HTParentAnchor *anchor=HTRequest_anchor(HTRequest_source(request)); long cl = HTAnchor_length(anchor); if (cl > 0) { long b_write = HTRequest_bodyWritten(request); double pro = (double) b_write/cl*100; char buf[10]; char pct[10]; HTNumToStr((unsigned long) cl, buf, 10); sprintf(pct, "%d%%", (int) pro); StrAllocMCopy(&result, "Writing (", pct, " of ", buf, ")", NULL); } else { long b_written = HTRequest_bytesWritten(request); int * raw_written = input ? (int *) input : NULL; if (b_written > 0) { char buf[10]; HTNumToStr(b_written>0 ? b_written : 0, buf, 10); StrAllocMCopy(&result, "Writing ", buf, "bytes", NULL); } if (raw_written && *raw_written>0) { char buf[10]; HTNumToStr(*raw_written, buf, 10); StrAllocMCopy(&result, "Writing ", buf, "bytes", NULL); } else { StrAllocCopy(result, "Writing..."); } } } break; case HT_PROG_DONE: StrAllocCopy(result, "Done!"); break; case HT_PROG_INTERRUPT: StrAllocCopy(result, "Interrupted!"); break; case HT_PROG_OTHER: StrAllocCopy(result, "Working - please wait..."); break; case HT_PROG_TIMEOUT: StrAllocCopy(result, "Request timeout - server did not respond."); break; default: StrAllocCopy(result, "UNKNOWN PROGRESS STATE"); break; } return result; }