Tweak CHANGES.
[polipo.git] / local.c
blob82106c4e3000d2665579d8bb61cbd0e3b4abaf30
1 /*
2 Copyright (c) 2003-2006 by Juliusz Chroboczek
4 Permission is hereby granted, free of charge, to any person obtaining a copy
5 of this software and associated documentation files (the "Software"), to deal
6 in the Software without restriction, including without limitation the rights
7 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 copies of the Software, and to permit persons to whom the Software is
9 furnished to do so, subject to the following conditions:
11 The above copyright notice and this permission notice shall be included in
12 all copies or substantial portions of the Software.
14 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20 THE SOFTWARE.
23 #include "polipo.h"
25 int disableLocalInterface = 0;
26 int disableConfiguration = 0;
27 int disableIndexing = 1;
28 int disableServersList = 1;
30 AtomPtr atomInitForbidden;
31 AtomPtr atomReopenLog;
32 AtomPtr atomDiscardObjects;
33 AtomPtr atomWriteoutObjects;
34 AtomPtr atomFreeChunkArenas;
36 void
37 preinitLocal()
39 atomInitForbidden = internAtom("init-forbidden");
40 atomReopenLog = internAtom("reopen-log");
41 atomDiscardObjects = internAtom("discard-objects");
42 atomWriteoutObjects = internAtom("writeout-objects");
43 atomFreeChunkArenas = internAtom("free-chunk-arenas");
45 /* These should not be settable for obvious reasons */
46 CONFIG_VARIABLE(disableLocalInterface, CONFIG_BOOLEAN,
47 "Disable the local configuration pages.");
48 CONFIG_VARIABLE(disableConfiguration, CONFIG_BOOLEAN,
49 "Disable reconfiguring Polipo at runtime.");
50 CONFIG_VARIABLE(disableIndexing, CONFIG_BOOLEAN,
51 "Disable indexing of the local cache.");
52 CONFIG_VARIABLE(disableServersList, CONFIG_BOOLEAN,
53 "Disable the list of known servers.");
56 static void fillSpecialObject(ObjectPtr, void (*)(FILE*, char*), void*);
58 int
59 httpLocalRequest(ObjectPtr object, int method, int from, int to,
60 HTTPRequestPtr requestor, void *closure)
62 if(object->requestor == NULL)
63 object->requestor = requestor;
65 if(!disableLocalInterface && urlIsSpecial(object->key, object->key_size))
66 return httpSpecialRequest(object, method, from, to,
67 requestor, closure);
69 if(method >= METHOD_POST) {
70 httpClientError(requestor, 405, internAtom("Method not allowed"));
71 requestor->connection->flags &= ~CONN_READER;
72 return 1;
75 /* objectFillFromDisk already did the real work but we have to
76 make sure we don't get into an infinite loop. */
77 if(object->flags & OBJECT_INITIAL) {
78 abortObject(object, 404, internAtom("Not found"));
80 object->age = current_time.tv_sec;
81 object->date = current_time.tv_sec;
83 object->flags &= ~OBJECT_VALIDATING;
84 notifyObject(object);
85 return 1;
88 void
89 alternatingHttpStyle(FILE *out, char *id)
91 fprintf(out,
92 "<style type=\"text/css\">\n"
93 "#%s tbody tr.even td { background-color: #eee; }\n"
94 "#%s tbody tr.odd td { background-color: #fff; }\n"
95 "</style>\n", id, id);
98 static void
99 printConfig(FILE *out, char *dummy)
101 fprintf(out,
102 "<!DOCTYPE HTML PUBLIC "
103 "\"-//W3C//DTD HTML 4.01 Transitional//EN\" "
104 "\"http://www.w3.org/TR/html4/loose.dtd\">\n"
105 "<html><head>\n"
106 "<title>Polipo configuration</title>\n"
107 "</head><body>\n"
108 "<h1>Polipo configuration</h1>\n");
109 printConfigVariables(out, 1);
110 fprintf(out, "<p><a href=\"/polipo/\">back</a></p>");
111 fprintf(out, "</body></html>\n");
114 #ifndef NO_DISK_CACHE
116 static void
117 recursiveIndexDiskObjects(FILE *out, char *root)
119 indexDiskObjects(out, root, 1);
122 static void
123 plainIndexDiskObjects(FILE *out, char *root)
125 indexDiskObjects(out, root, 0);
127 #endif
129 static void
130 serversList(FILE *out, char *dummy)
132 listServers(out);
135 static int
136 matchUrl(char *base, ObjectPtr object)
138 int n = strlen(base);
139 if(object->key_size < n)
140 return 0;
141 if(memcmp(base, object->key, n) != 0)
142 return 0;
143 return (object->key_size == n) || (((char*)object->key)[n] == '?');
146 int
147 httpSpecialRequest(ObjectPtr object, int method, int from, int to,
148 HTTPRequestPtr requestor, void *closure)
150 char buffer[1024];
151 int hlen;
153 if(method >= METHOD_POST) {
154 return httpSpecialSideRequest(object, method, from, to,
155 requestor, closure);
158 if(!(object->flags & OBJECT_INITIAL)) {
159 privatiseObject(object, 0);
160 supersedeObject(object);
161 object->flags &= ~(OBJECT_VALIDATING | OBJECT_INPROGRESS);
162 notifyObject(object);
163 return 1;
166 hlen = snnprintf(buffer, 0, 1024,
167 "\r\nServer: polipo"
168 "\r\nContent-Type: text/html");
169 object->date = current_time.tv_sec;
170 object->age = current_time.tv_sec;
171 object->headers = internAtomN(buffer, hlen);
172 object->code = 200;
173 object->message = internAtom("Okay");
174 object->flags &= ~OBJECT_INITIAL;
175 object->flags |= OBJECT_DYNAMIC;
177 if(object->key_size == 8 && memcmp(object->key, "/polipo/", 8) == 0) {
178 objectPrintf(object, 0,
179 "<!DOCTYPE HTML PUBLIC "
180 "\"-//W3C//DTD HTML 4.01 Transitional//EN\" "
181 "\"http://www.w3.org/TR/html4/loose.dtd\">\n"
182 "<html><head>\n"
183 "<title>Polipo</title>\n"
184 "</head><body>\n"
185 "<h1>Polipo</h1>\n"
186 "<p><a href=\"status?\">Status report</a>.</p>\n"
187 "<p><a href=\"config?\">Current configuration</a>.</p>\n"
188 "<p><a href=\"servers?\">Known servers</a>.</p>\n"
189 #ifndef NO_DISK_CACHE
190 "<p><a href=\"index?\">Disk cache index</a>.</p>\n"
191 #endif
192 "</body></html>\n");
193 object->length = object->size;
194 } else if(matchUrl("/polipo/status", object)) {
195 objectPrintf(object, 0,
196 "<!DOCTYPE HTML PUBLIC "
197 "\"-//W3C//DTD HTML 4.01 Transitional//EN\" "
198 "\"http://www.w3.org/TR/html4/loose.dtd\">\n"
199 "<html><head>\n"
200 "<title>Polipo status report</title>\n"
201 "</head><body>\n"
202 "<h1>Polipo proxy on %s:%d: status report</h1>\n"
203 "<p>The %s proxy on %s:%d is %s.</p>\n"
204 "<p>There are %d public and %d private objects "
205 "currently in memory using %d KB in %d chunks "
206 "(%d KB allocated).</p>\n"
207 "<p>There are %d atoms.</p>"
208 "<p><form method=POST action=\"/polipo/status?\">"
209 "<input type=submit name=\"init-forbidden\" "
210 "value=\"Read forbidden file\"></form>\n"
211 "<form method=POST action=\"/polipo/status?\">"
212 "<input type=submit name=\"writeout-objects\" "
213 "value=\"Write out in-memory cache\"></form>\n"
214 "<form method=POST action=\"/polipo/status?\">"
215 "<input type=submit name=\"discard-objects\" "
216 "value=\"Discard in-memory cache\"></form>\n"
217 "<form method=POST action=\"/polipo/status?\">"
218 "<input type=submit name=\"reopen-log\" "
219 "value=\"Reopen log file\"></form>\n"
220 "<form method=POST action=\"/polipo/status?\">"
221 "<input type=submit name=\"free-chunk-arenas\" "
222 "value=\"Free chunk arenas\"></form></p>\n"
223 "<p><a href=\"/polipo/\">back</a></p>"
224 "</body></html>\n",
225 proxyName->string, proxyPort,
226 cacheIsShared ? "shared" : "private",
227 proxyName->string, proxyPort,
228 proxyOffline ? "off line" :
229 (relaxTransparency ?
230 "on line (transparency relaxed)" :
231 "on line"),
232 publicObjectCount, privateObjectCount,
233 used_chunks * CHUNK_SIZE / 1024, used_chunks,
234 totalChunkArenaSize() / 1024,
235 used_atoms);
236 object->expires = current_time.tv_sec;
237 object->length = object->size;
238 } else if(matchUrl("/polipo/config", object)) {
239 fillSpecialObject(object, printConfig, NULL);
240 object->expires = current_time.tv_sec + 5;
241 #ifndef NO_DISK_CACHE
242 } else if(matchUrl("/polipo/index", object)) {
243 int len;
244 char *root;
245 if(disableIndexing) {
246 abortObject(object, 403, internAtom("Action not allowed"));
247 notifyObject(object);
248 return 1;
250 len = MAX(0, object->key_size - 14);
251 root = strdup_n((char*)object->key + 14, len);
252 if(root == NULL) {
253 abortObject(object, 503, internAtom("Couldn't allocate root"));
254 notifyObject(object);
255 return 1;
257 writeoutObjects(1);
258 fillSpecialObject(object, plainIndexDiskObjects, root);
259 free(root);
260 object->expires = current_time.tv_sec + 5;
261 } else if(matchUrl("/polipo/recursive-index", object)) {
262 int len;
263 char *root;
264 if(disableIndexing) {
265 abortObject(object, 403, internAtom("Action not allowed"));
266 notifyObject(object);
267 return 1;
269 len = MAX(0, object->key_size - 24);
270 root = strdup_n((char*)object->key + 24, len);
271 if(root == NULL) {
272 abortObject(object, 503, internAtom("Couldn't allocate root"));
273 notifyObject(object);
274 return 1;
276 writeoutObjects(1);
277 fillSpecialObject(object, recursiveIndexDiskObjects, root);
278 free(root);
279 object->expires = current_time.tv_sec + 20;
280 #endif
281 } else if(matchUrl("/polipo/servers", object)) {
282 if(disableServersList) {
283 abortObject(object, 403, internAtom("Action not allowed"));
284 notifyObject(object);
285 return 1;
287 fillSpecialObject(object, serversList, NULL);
288 object->expires = current_time.tv_sec + 2;
289 } else {
290 abortObject(object, 404, internAtom("Not found"));
293 object->flags &= ~OBJECT_VALIDATING;
294 notifyObject(object);
295 return 1;
298 int
299 httpSpecialSideRequest(ObjectPtr object, int method, int from, int to,
300 HTTPRequestPtr requestor, void *closure)
302 HTTPConnectionPtr client = requestor->connection;
304 assert(client->request == requestor);
306 if(method != METHOD_POST) {
307 httpClientError(requestor, 405, internAtom("Method not allowed"));
308 requestor->connection->flags &= ~CONN_READER;
309 return 1;
312 return httpSpecialDoSide(requestor);
316 httpSpecialDoSide(HTTPRequestPtr requestor)
318 HTTPConnectionPtr client = requestor->connection;
320 if(client->reqlen - client->reqbegin >= client->bodylen) {
321 AtomPtr data;
322 data = internAtomN(client->reqbuf + client->reqbegin,
323 client->reqlen - client->reqbegin);
324 client->reqbegin = 0;
325 client->reqlen = 0;
326 if(data == NULL) {
327 do_log(L_ERROR, "Couldn't allocate data.\n");
328 httpClientError(requestor, 500,
329 internAtom("Couldn't allocate data"));
330 return 1;
332 httpSpecialDoSideFinish(data, requestor);
333 return 1;
336 if(client->reqlen - client->reqbegin >= CHUNK_SIZE) {
337 httpClientError(requestor, 500, internAtom("POST too large"));
338 return 1;
341 if(client->reqbegin > 0 && client->reqlen > client->reqbegin) {
342 memmove(client->reqbuf, client->reqbuf + client->reqbegin,
343 client->reqlen - client->reqbegin);
345 client->reqlen -= client->reqbegin;
346 client->reqbegin = 0;
348 do_stream(IO_READ | IO_NOTNOW, client->fd,
349 client->reqlen, client->reqbuf, CHUNK_SIZE,
350 httpSpecialClientSideHandler, client);
351 return 1;
355 httpSpecialClientSideHandler(int status,
356 FdEventHandlerPtr event,
357 StreamRequestPtr srequest)
359 HTTPConnectionPtr connection = srequest->data;
360 HTTPRequestPtr request = connection->request;
361 int push;
363 if((request->object->flags & OBJECT_ABORTED) ||
364 !(request->object->flags & OBJECT_INPROGRESS)) {
365 httpClientDiscardBody(connection);
366 httpClientError(request, 503, internAtom("Post aborted"));
367 return 1;
370 if(status < 0) {
371 do_log_error(L_ERROR, -status, "Reading from client");
372 if(status == -EDOGRACEFUL)
373 httpClientFinish(connection, 1);
374 else
375 httpClientFinish(connection, 2);
376 return 1;
379 push = MIN(srequest->offset - connection->reqlen,
380 connection->bodylen - connection->reqoffset);
381 if(push > 0) {
382 connection->reqlen += push;
383 httpSpecialDoSide(request);
386 do_log(L_ERROR, "Incomplete client request.\n");
387 connection->flags &= ~CONN_READER;
388 httpClientRawError(connection, 502,
389 internAtom("Incomplete client request"), 1);
390 return 1;
394 httpSpecialDoSideFinish(AtomPtr data, HTTPRequestPtr requestor)
396 ObjectPtr object = requestor->object;
398 if(matchUrl("/polipo/config", object)) {
399 AtomListPtr list = NULL;
400 int i, rc;
402 if(disableConfiguration) {
403 abortObject(object, 403, internAtom("Action not allowed"));
404 goto out;
407 list = urlDecode(data->string, data->length);
408 if(list == NULL) {
409 abortObject(object, 400,
410 internAtom("Couldn't parse variable to set"));
411 goto out;
413 for(i = 0; i < list->length; i++) {
414 rc = parseConfigLine(list->list[i]->string, NULL, 0, 1);
415 if(rc < 0) {
416 abortObject(object, 400,
417 rc == -1 ?
418 internAtom("Couldn't parse variable to set") :
419 internAtom("Variable is not settable"));
420 destroyAtomList(list);
421 goto out;
424 destroyAtomList(list);
425 object->date = current_time.tv_sec;
426 object->age = current_time.tv_sec;
427 object->headers = internAtom("\r\nLocation: /polipo/config?");
428 object->code = 303;
429 object->message = internAtom("Done");
430 object->flags &= ~OBJECT_INITIAL;
431 object->length = 0;
432 } else if(matchUrl("/polipo/status", object)) {
433 AtomListPtr list = NULL;
434 int i;
436 if(disableConfiguration) {
437 abortObject(object, 403, internAtom("Action not allowed"));
438 goto out;
441 list = urlDecode(data->string, data->length);
442 if(list == NULL) {
443 abortObject(object, 400,
444 internAtom("Couldn't parse action"));
445 goto out;
447 for(i = 0; i < list->length; i++) {
448 char *equals =
449 memchr(list->list[i]->string, '=', list->list[i]->length);
450 AtomPtr name =
451 equals ?
452 internAtomN(list->list[i]->string,
453 equals - list->list[i]->string) :
454 retainAtom(list->list[i]);
455 if(name == atomInitForbidden)
456 initForbidden();
457 else if(name == atomReopenLog)
458 reopenLog();
459 else if(name == atomDiscardObjects)
460 discardObjects(1, 0);
461 else if(name == atomWriteoutObjects)
462 writeoutObjects(1);
463 else if(name == atomFreeChunkArenas)
464 free_chunk_arenas();
465 else {
466 abortObject(object, 400, internAtomF("Unknown action %s",
467 name->string));
468 releaseAtom(name);
469 destroyAtomList(list);
470 goto out;
472 releaseAtom(name);
474 destroyAtomList(list);
475 object->date = current_time.tv_sec;
476 object->age = current_time.tv_sec;
477 object->headers = internAtom("\r\nLocation: /polipo/status?");
478 object->code = 303;
479 object->message = internAtom("Done");
480 object->flags &= ~OBJECT_INITIAL;
481 object->length = 0;
482 } else {
483 abortObject(object, 405, internAtom("Method not allowed"));
486 out:
487 releaseAtom(data);
488 notifyObject(object);
489 requestor->connection->flags &= ~CONN_READER;
490 return 1;
493 #ifdef HAVE_FORK
494 static void
495 fillSpecialObject(ObjectPtr object, void (*fn)(FILE*, char*), void* closure)
497 int rc;
498 int filedes[2];
499 pid_t pid;
500 sigset_t ss, old_mask;
502 if(object->flags & OBJECT_INPROGRESS)
503 return;
505 rc = pipe(filedes);
506 if(rc < 0) {
507 do_log_error(L_ERROR, errno, "Couldn't create pipe");
508 abortObject(object, 503,
509 internAtomError(errno, "Couldn't create pipe"));
510 return;
513 fflush(stdout);
514 fflush(stderr);
515 flushLog();
517 /* Block signals that we handle specially until the child can
518 disable the handlers. */
519 interestingSignals(&ss);
520 /* I'm a little confused. POSIX doesn't allow EINTR here, but I
521 think that both Linux and SVR4 do. */
522 do {
523 rc = sigprocmask(SIG_BLOCK, &ss, &old_mask);
524 } while (rc < 0 && errno == EINTR);
525 if(rc < 0) {
526 do_log_error(L_ERROR, errno, "Sigprocmask failed");
527 abortObject(object, 503, internAtomError(errno, "Sigprocmask failed"));
528 close(filedes[0]);
529 close(filedes[1]);
530 return;
533 pid = fork();
534 if(pid < 0) {
535 do_log_error(L_ERROR, errno, "Couldn't fork");
536 abortObject(object, 503, internAtomError(errno, "Couldn't fork"));
537 close(filedes[0]);
538 close(filedes[1]);
539 do {
540 rc = sigprocmask(SIG_SETMASK, &old_mask, NULL);
541 } while (rc < 0 && errno == EINTR);
542 if(rc < 0) {
543 do_log_error(L_ERROR, errno, "Couldn't restore signal mask");
544 polipoExit();
546 return;
549 if(pid > 0) {
550 SpecialRequestPtr request;
551 close(filedes[1]);
552 do {
553 rc = sigprocmask(SIG_SETMASK, &old_mask, NULL);
554 } while (rc < 0 && errno == EINTR);
555 if(rc < 0) {
556 do_log_error(L_ERROR, errno, "Couldn't restore signal mask");
557 polipoExit();
558 return;
561 request = malloc(sizeof(SpecialRequestRec));
562 if(request == NULL) {
563 kill(pid, SIGTERM);
564 close(filedes[0]);
565 abortObject(object, 503,
566 internAtom("Couldn't allocate request\n"));
567 notifyObject(object);
568 /* specialRequestHandler will take care of the rest. */
569 } else {
570 request->buf = get_chunk();
571 if(request->buf == NULL) {
572 kill(pid, SIGTERM);
573 close(filedes[0]);
574 free(request);
575 abortObject(object, 503,
576 internAtom("Couldn't allocate request\n"));
577 notifyObject(object);
580 object->flags |= OBJECT_INPROGRESS;
581 retainObject(object);
582 request->object = object;
583 request->fd = filedes[0];
584 request->pid = pid;
585 request->offset = 0;
586 /* Under any sensible scheduler, the child will run before the
587 parent. So no need for IO_NOTNOW. */
588 do_stream(IO_READ, filedes[0], 0, request->buf, CHUNK_SIZE,
589 specialRequestHandler, request);
590 } else {
591 /* child */
592 close(filedes[0]);
593 uninitEvents();
594 do {
595 rc = sigprocmask(SIG_SETMASK, &old_mask, NULL);
596 } while (rc < 0 && errno == EINTR);
597 if(rc < 0)
598 exit(1);
600 if(filedes[1] != 1)
601 dup2(filedes[1], 1);
603 (*fn)(stdout, closure);
604 exit(0);
609 specialRequestHandler(int status,
610 FdEventHandlerPtr event, StreamRequestPtr srequest)
612 SpecialRequestPtr request = srequest->data;
613 int rc;
614 int killed = 0;
616 if(status < 0) {
617 kill(request->pid, SIGTERM);
618 killed = 1;
619 request->object->flags &= ~OBJECT_INPROGRESS;
620 abortObject(request->object, 502,
621 internAtomError(-status, "Couldn't read from client"));
622 goto done;
625 if(srequest->offset > 0) {
626 rc = objectAddData(request->object, request->buf,
627 request->offset, srequest->offset);
628 if(rc < 0) {
629 kill(request->pid, SIGTERM);
630 killed = 1;
631 request->object->flags &= ~OBJECT_INPROGRESS;
632 abortObject(request->object, 503,
633 internAtom("Couldn't add data to connection"));
634 goto done;
636 request->offset += srequest->offset;
638 if(status) {
639 request->object->flags &= ~OBJECT_INPROGRESS;
640 request->object->length = request->object->size;
641 goto done;
644 /* If we're the only person interested in this object, let's abort
645 it now. */
646 if(request->object->refcount <= 1) {
647 kill(request->pid, SIGTERM);
648 killed = 1;
649 request->object->flags &= ~OBJECT_INPROGRESS;
650 abortObject(request->object, 500, internAtom("Aborted"));
651 goto done;
653 notifyObject(request->object);
654 do_stream(IO_READ | IO_NOTNOW, request->fd, 0, request->buf, CHUNK_SIZE,
655 specialRequestHandler, request);
656 return 1;
658 done:
659 close(request->fd);
660 dispose_chunk(request->buf);
661 releaseNotifyObject(request->object);
662 /* That's a blocking wait. It shouldn't block for long, as we've
663 either already killed the child, or else we got EOF from it. */
664 do {
665 rc = waitpid(request->pid, &status, 0);
666 } while(rc < 0 && errno == EINTR);
667 if(rc < 0) {
668 do_log(L_ERROR, "Wait for %d: %d\n", (int)request->pid, errno);
669 } else {
670 int normal =
671 (WIFEXITED(status) && WEXITSTATUS(status) == 0) ||
672 (killed && WIFSIGNALED(status) && WTERMSIG(status) == SIGTERM);
673 char *reason =
674 WIFEXITED(status) ? "with status" :
675 WIFSIGNALED(status) ? "on signal" :
676 "with unknown status";
677 int value =
678 WIFEXITED(status) ? WEXITSTATUS(status) :
679 WIFSIGNALED(status) ? WTERMSIG(status) :
680 status;
681 do_log(normal ? D_CHILD : L_ERROR,
682 "Child %d exited %s %d.\n",
683 (int)request->pid, reason, value);
685 free(request);
686 return 1;
688 #else
689 static void
690 fillSpecialObject(ObjectPtr object, void (*fn)(FILE*, char*), void* closure)
692 FILE *tmp = NULL;
693 char *buf = NULL;
694 int rc, len, offset;
696 if(object->flags & OBJECT_INPROGRESS)
697 return;
699 buf = get_chunk();
700 if(buf == NULL) {
701 abortObject(object, 503, internAtom("Couldn't allocate chunk"));
702 goto done;
705 tmp = tmpfile();
706 if(tmp == NULL) {
707 abortObject(object, 503, internAtom(pstrerror(errno)));
708 goto done;
711 (*fn)(tmp, closure);
712 fflush(tmp);
714 rewind(tmp);
715 offset = 0;
716 while(1) {
717 len = fread(buf, 1, CHUNK_SIZE, tmp);
718 if(len <= 0 && ferror(tmp)) {
719 abortObject(object, 503, internAtom(pstrerror(errno)));
720 goto done;
722 if(len <= 0)
723 break;
725 rc = objectAddData(object, buf, offset, len);
726 if(rc < 0) {
727 abortObject(object, 503, internAtom("Couldn't add data to object"));
728 goto done;
731 offset += len;
734 object->length = offset;
736 done:
737 if(buf)
738 dispose_chunk(buf);
739 if(tmp)
740 fclose(tmp);
741 notifyObject(object);
743 #endif