]>
Commit | Line | Data |
---|---|---|
da2b3a17 | 1 | |
30a4f2a8 | 2 | /* |
c8fd0193 | 3 | * $Id: http.cc,v 1.320 1998/09/15 20:36:15 wessels Exp $ |
30a4f2a8 | 4 | * |
5 | * DEBUG: section 11 Hypertext Transfer Protocol (HTTP) | |
6 | * AUTHOR: Harvest Derived | |
7 | * | |
42c04c16 | 8 | * SQUID Internet Object Cache http://squid.nlanr.net/Squid/ |
e25c139f | 9 | * ---------------------------------------------------------- |
30a4f2a8 | 10 | * |
11 | * Squid is the result of efforts by numerous individuals from the | |
12 | * Internet community. Development is led by Duane Wessels of the | |
e25c139f | 13 | * National Laboratory for Applied Network Research and funded by the |
14 | * National Science Foundation. Squid is Copyrighted (C) 1998 by | |
15 | * Duane Wessels and the University of California San Diego. Please | |
16 | * see the COPYRIGHT file for full details. Squid incorporates | |
17 | * software developed and/or copyrighted by other sources. Please see | |
18 | * the CREDITS file for full details. | |
30a4f2a8 | 19 | * |
20 | * This program is free software; you can redistribute it and/or modify | |
21 | * it under the terms of the GNU General Public License as published by | |
22 | * the Free Software Foundation; either version 2 of the License, or | |
23 | * (at your option) any later version. | |
24 | * | |
25 | * This program is distributed in the hope that it will be useful, | |
26 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | |
27 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
28 | * GNU General Public License for more details. | |
29 | * | |
30 | * You should have received a copy of the GNU General Public License | |
31 | * along with this program; if not, write to the Free Software | |
cbdec147 | 32 | * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111, USA. |
e25c139f | 33 | * |
30a4f2a8 | 34 | */ |
019dd986 | 35 | |
4a83b852 | 36 | /* |
37 | * Anonymizing patch by lutz@as-node.jena.thur.de | |
de3bdb4c | 38 | * have a look into http-anon.c to get more informations. |
4a83b852 | 39 | */ |
40 | ||
44a47c6e | 41 | #include "squid.h" |
090089c4 | 42 | |
6bf8443a | 43 | static const char *const crlf = "\r\n"; |
4db43fab | 44 | |
9e4ad609 | 45 | static CNCB httpConnectDone; |
46 | static CWCB httpSendComplete; | |
54220df8 | 47 | static CWCB httpSendRequestEntry; |
48 | ||
9e4ad609 | 49 | static PF httpReadReply; |
50 | static PF httpSendRequest; | |
51 | static PF httpStateFree; | |
52 | static PF httpTimeout; | |
f5b8bbc4 | 53 | static void httpCacheNegatively(StoreEntry *); |
54 | static void httpMakePrivate(StoreEntry *); | |
55 | static void httpMakePublic(StoreEntry *); | |
f8309b15 | 56 | static int httpCachableReply(HttpStateData *); |
f9cece6e | 57 | static void httpMaybeRemovePublic(StoreEntry *, http_status); |
b8d8561b | 58 | |
b177367b | 59 | static void |
79d39a72 | 60 | httpStateFree(int fdnotused, void *data) |
f5558c95 | 61 | { |
b177367b | 62 | HttpStateData *httpState = data; |
0d4d4170 | 63 | if (httpState == NULL) |
b177367b | 64 | return; |
f88211e8 | 65 | storeUnlockObject(httpState->entry); |
0d4d4170 | 66 | if (httpState->reply_hdr) { |
3f6c0fb2 | 67 | memFree(MEM_8K_BUF, httpState->reply_hdr); |
0d4d4170 | 68 | httpState->reply_hdr = NULL; |
69 | } | |
30a4f2a8 | 70 | requestUnlink(httpState->request); |
20cc1450 | 71 | requestUnlink(httpState->orig_request); |
7dd44885 | 72 | httpState->request = NULL; |
73 | httpState->orig_request = NULL; | |
74 | cbdataFree(httpState); | |
f5558c95 | 75 | } |
76 | ||
b8d8561b | 77 | int |
75e88d56 | 78 | httpCachable(method_t method) |
090089c4 | 79 | { |
090089c4 | 80 | /* GET and HEAD are cachable. Others are not. */ |
6eb42cae | 81 | if (method != METHOD_GET && method != METHOD_HEAD) |
090089c4 | 82 | return 0; |
090089c4 | 83 | /* else cachable */ |
84 | return 1; | |
85 | } | |
86 | ||
b8d8561b | 87 | static void |
5c5783a2 | 88 | httpTimeout(int fd, void *data) |
090089c4 | 89 | { |
b177367b | 90 | HttpStateData *httpState = data; |
593c9a75 | 91 | StoreEntry *entry = httpState->entry; |
9b312a19 | 92 | ErrorState *err; |
9fb13bb6 | 93 | debug(11, 4) ("httpTimeout: FD %d: '%s'\n", fd, storeUrl(entry)); |
8796b9e9 | 94 | assert(entry->store_status == STORE_PENDING); |
73a3014d | 95 | if (entry->mem_obj->inmem_hi == 0) { |
fe40a877 | 96 | err = errorCon(ERR_READ_TIMEOUT, HTTP_GATEWAY_TIMEOUT); |
79a15e0a | 97 | err->request = requestLink(httpState->orig_request); |
9b312a19 | 98 | errorAppendEntry(entry, err); |
b50179a6 | 99 | } else { |
b34ed725 | 100 | storeAbort(entry, 0); |
9b312a19 | 101 | } |
0d4d4170 | 102 | comm_close(fd); |
090089c4 | 103 | } |
104 | ||
30a4f2a8 | 105 | /* This object can be cached for a long time */ |
b8d8561b | 106 | static void |
107 | httpMakePublic(StoreEntry * entry) | |
30a4f2a8 | 108 | { |
415e0dd2 | 109 | if (entry->flags.entry_cachable) |
30a4f2a8 | 110 | storeSetPublicKey(entry); |
111 | } | |
112 | ||
113 | /* This object should never be cached at all */ | |
b8d8561b | 114 | static void |
115 | httpMakePrivate(StoreEntry * entry) | |
30a4f2a8 | 116 | { |
30a4f2a8 | 117 | storeExpireNow(entry); |
30a4f2a8 | 118 | storeReleaseRequest(entry); /* delete object when not used */ |
f3e570e9 | 119 | /* storeReleaseRequest clears ENTRY_CACHABLE flag */ |
30a4f2a8 | 120 | } |
121 | ||
122 | /* This object may be negatively cached */ | |
b8d8561b | 123 | static void |
124 | httpCacheNegatively(StoreEntry * entry) | |
30a4f2a8 | 125 | { |
79b5cc5f | 126 | storeNegativeCache(entry); |
415e0dd2 | 127 | if (entry->flags.entry_cachable) |
30a4f2a8 | 128 | storeSetPublicKey(entry); |
30a4f2a8 | 129 | } |
130 | ||
f9cece6e | 131 | static void |
132 | httpMaybeRemovePublic(StoreEntry * e, http_status status) | |
133 | { | |
134 | int remove = 0; | |
135 | const cache_key *key; | |
136 | StoreEntry *pe; | |
415e0dd2 | 137 | if (!e->flags.key_private) |
9dc1202d | 138 | return; |
f9cece6e | 139 | switch (status) { |
140 | case HTTP_OK: | |
141 | case HTTP_NON_AUTHORITATIVE_INFORMATION: | |
142 | case HTTP_MULTIPLE_CHOICES: | |
143 | case HTTP_MOVED_PERMANENTLY: | |
144 | case HTTP_MOVED_TEMPORARILY: | |
145 | case HTTP_FORBIDDEN: | |
146 | case HTTP_NOT_FOUND: | |
147 | case HTTP_METHOD_NOT_ALLOWED: | |
148 | case HTTP_GONE: | |
149 | remove = 1; | |
150 | break; | |
151 | #if WORK_IN_PROGRESS | |
c8fd0193 | 152 | case HTTP_UNAUTHORIZED: |
153 | remove = 1; | |
f9cece6e | 154 | break; |
155 | #endif | |
156 | default: | |
157 | remove = 0; | |
158 | break; | |
159 | } | |
160 | if (!remove) | |
161 | return; | |
162 | assert(e->mem_obj); | |
163 | key = storeKeyPublic(e->mem_obj->url, e->mem_obj->method); | |
164 | if ((pe = storeGet(key)) == NULL) | |
165 | return; | |
9dc1202d | 166 | assert(e != pe); |
f9cece6e | 167 | storeRelease(pe); |
168 | } | |
169 | ||
f8309b15 | 170 | static int |
171 | httpCachableReply(HttpStateData * httpState) | |
c54e9052 | 172 | { |
d8b249ef | 173 | HttpReply *rep = httpState->entry->mem_obj->reply; |
174 | HttpHeader *hdr = &rep->header; | |
175 | const int cc_mask = (rep->cache_control) ? rep->cache_control->mask : 0; | |
7faf2bdb | 176 | if (EBIT_TEST(cc_mask, CC_PRIVATE)) |
f8309b15 | 177 | return 0; |
7faf2bdb | 178 | if (EBIT_TEST(cc_mask, CC_NO_CACHE)) |
f8309b15 | 179 | return 0; |
ed2f05a1 | 180 | if (EBIT_TEST(cc_mask, CC_NO_STORE)) |
181 | return 0; | |
92695e5e | 182 | if (httpState->request->flags.auth) { |
a6dfe2d9 | 183 | /* |
184 | * Responses to requests with authorization may be cached | |
68aefb7d | 185 | * only if a Cache-Control: public reply header is present. |
a6dfe2d9 | 186 | * RFC 2068, sec 14.9.4 |
187 | */ | |
188 | if (!EBIT_TEST(cc_mask, CC_PUBLIC)) | |
fee0cebb | 189 | return 0; |
a6dfe2d9 | 190 | } |
f8309b15 | 191 | /* |
02fe0fbc | 192 | * We don't properly deal with Vary features yet, so we can't |
193 | * cache these | |
f8309b15 | 194 | */ |
783e4699 | 195 | if (httpHeaderHas(hdr, HDR_VARY)) |
196 | return 0; | |
cb69b4c7 | 197 | switch (httpState->entry->mem_obj->reply->sline.status) { |
c54e9052 | 198 | /* Responses that are cacheable */ |
19a04dac | 199 | case HTTP_OK: |
200 | case HTTP_NON_AUTHORITATIVE_INFORMATION: | |
201 | case HTTP_MULTIPLE_CHOICES: | |
202 | case HTTP_MOVED_PERMANENTLY: | |
203 | case HTTP_GONE: | |
1294c0fc | 204 | /* don't cache objects from peers w/o LMT, Date, or Expires */ |
cb69b4c7 | 205 | /* check that is it enough to check headers @?@ */ |
d8b249ef | 206 | if (rep->date > -1) |
c54e9052 | 207 | return 1; |
d8b249ef | 208 | else if (rep->last_modified > -1) |
c54e9052 | 209 | return 1; |
1294c0fc | 210 | else if (!httpState->peer) |
c54e9052 | 211 | return 1; |
d8b249ef | 212 | /* @?@ (here and 302): invalid expires header compiles to squid_curtime */ |
213 | else if (rep->expires > -1) | |
c54e9052 | 214 | return 1; |
c54e9052 | 215 | else |
216 | return 0; | |
79d39a72 | 217 | /* NOTREACHED */ |
c54e9052 | 218 | break; |
219 | /* Responses that only are cacheable if the server says so */ | |
19a04dac | 220 | case HTTP_MOVED_TEMPORARILY: |
d8b249ef | 221 | if (rep->expires > -1) |
c54e9052 | 222 | return 1; |
223 | else | |
224 | return 0; | |
79d39a72 | 225 | /* NOTREACHED */ |
c54e9052 | 226 | break; |
227 | /* Errors can be negatively cached */ | |
19a04dac | 228 | case HTTP_NO_CONTENT: |
229 | case HTTP_USE_PROXY: | |
230 | case HTTP_BAD_REQUEST: | |
231 | case HTTP_FORBIDDEN: | |
232 | case HTTP_NOT_FOUND: | |
233 | case HTTP_METHOD_NOT_ALLOWED: | |
234 | case HTTP_REQUEST_URI_TOO_LARGE: | |
235 | case HTTP_INTERNAL_SERVER_ERROR: | |
236 | case HTTP_NOT_IMPLEMENTED: | |
237 | case HTTP_BAD_GATEWAY: | |
238 | case HTTP_SERVICE_UNAVAILABLE: | |
239 | case HTTP_GATEWAY_TIMEOUT: | |
c54e9052 | 240 | return -1; |
79d39a72 | 241 | /* NOTREACHED */ |
c54e9052 | 242 | break; |
243 | /* Some responses can never be cached */ | |
0cdcddb9 | 244 | case HTTP_PARTIAL_CONTENT: /* Not yet supported */ |
19a04dac | 245 | case HTTP_SEE_OTHER: |
246 | case HTTP_NOT_MODIFIED: | |
247 | case HTTP_UNAUTHORIZED: | |
248 | case HTTP_PROXY_AUTHENTICATION_REQUIRED: | |
0cdcddb9 | 249 | case HTTP_INVALID_HEADER: /* Squid header parsing error */ |
c54e9052 | 250 | default: /* Unknown status code */ |
251 | return 0; | |
79d39a72 | 252 | /* NOTREACHED */ |
c54e9052 | 253 | break; |
254 | } | |
79d39a72 | 255 | /* NOTREACHED */ |
c54e9052 | 256 | } |
090089c4 | 257 | |
cb69b4c7 | 258 | /* rewrite this later using new interfaces @?@ */ |
b8d8561b | 259 | void |
0ee4272b | 260 | httpProcessReplyHeader(HttpStateData * httpState, const char *buf, int size) |
f5558c95 | 261 | { |
262 | char *t = NULL; | |
30a4f2a8 | 263 | StoreEntry *entry = httpState->entry; |
d3fb4dea | 264 | int room; |
265 | int hdr_len; | |
cb69b4c7 | 266 | HttpReply *reply = entry->mem_obj->reply; |
b6cfb65c | 267 | debug(11, 3) ("httpProcessReplyHeader: key '%s'\n", |
268 | storeKeyText(entry->key)); | |
e924600d | 269 | if (httpState->reply_hdr == NULL) |
7021844c | 270 | httpState->reply_hdr = memAllocate(MEM_8K_BUF); |
30a4f2a8 | 271 | if (httpState->reply_hdr_state == 0) { |
272 | hdr_len = strlen(httpState->reply_hdr); | |
ed85b771 | 273 | room = 8191 - hdr_len; |
30a4f2a8 | 274 | strncat(httpState->reply_hdr, buf, room < size ? room : size); |
d3fb4dea | 275 | hdr_len += room < size ? room : size; |
30a4f2a8 | 276 | if (hdr_len > 4 && strncmp(httpState->reply_hdr, "HTTP/", 5)) { |
84fa351c | 277 | debug(11, 3) ("httpProcessReplyHeader: Non-HTTP-compliant header: '%s'\n", httpState->reply_hdr); |
30a4f2a8 | 278 | httpState->reply_hdr_state += 2; |
728da2ee | 279 | reply->sline.status = HTTP_INVALID_HEADER; |
ed85b771 | 280 | return; |
d3fb4dea | 281 | } |
d1a43e28 | 282 | t = httpState->reply_hdr + hdr_len; |
283 | /* headers can be incomplete only if object still arriving */ | |
2334c194 | 284 | if (!httpState->eof) { |
285 | size_t k = headersEnd(httpState->reply_hdr, 8192); | |
286 | if (0 == k) | |
d1a43e28 | 287 | return; /* headers not complete */ |
2334c194 | 288 | t = httpState->reply_hdr + k; |
289 | } | |
2285407f | 290 | *t = '\0'; |
30a4f2a8 | 291 | httpState->reply_hdr_state++; |
f5558c95 | 292 | } |
30a4f2a8 | 293 | if (httpState->reply_hdr_state == 1) { |
123abbe1 | 294 | const Ctx ctx = ctx_enter(entry->mem_obj->url); |
30a4f2a8 | 295 | httpState->reply_hdr_state++; |
a3d5953d | 296 | debug(11, 9) ("GOT HTTP REPLY HDR:\n---------\n%s\n----------\n", |
30a4f2a8 | 297 | httpState->reply_hdr); |
298 | /* Parse headers into reply structure */ | |
2246b732 | 299 | /* what happens if we fail to parse here? */ |
ee1679df | 300 | httpReplyParse(reply, httpState->reply_hdr); /* httpState->eof); */ |
ca98227c | 301 | storeTimestampsSet(entry); |
30a4f2a8 | 302 | /* Check if object is cacheable or not based on reply code */ |
cb69b4c7 | 303 | debug(11, 3) ("httpProcessReplyHeader: HTTP CODE: %d\n", reply->sline.status); |
9f239bed | 304 | if (neighbors_do_private_keys) |
305 | httpMaybeRemovePublic(entry, reply->sline.status); | |
f8309b15 | 306 | switch (httpCachableReply(httpState)) { |
c54e9052 | 307 | case 1: |
308 | httpMakePublic(entry); | |
30a4f2a8 | 309 | break; |
c54e9052 | 310 | case 0: |
311 | httpMakePrivate(entry); | |
f5558c95 | 312 | break; |
c54e9052 | 313 | case -1: |
851eeef7 | 314 | httpCacheNegatively(entry); |
30a4f2a8 | 315 | break; |
c54e9052 | 316 | default: |
317 | assert(0); | |
4e38e700 | 318 | break; |
f5558c95 | 319 | } |
0336304c | 320 | if (reply->cache_control) { |
321 | if (EBIT_TEST(reply->cache_control->mask, CC_PROXY_REVALIDATE)) | |
415e0dd2 | 322 | entry->flags.entry_revalidate = 1; |
308e4a84 | 323 | else if (EBIT_TEST(reply->cache_control->mask, CC_MUST_REVALIDATE)) |
415e0dd2 | 324 | entry->flags.entry_revalidate = 1; |
0336304c | 325 | } |
b515fc11 | 326 | if (httpState->flags.keepalive) |
9a47da71 | 327 | if (httpState->peer) |
328 | httpState->peer->stats.n_keepalives_sent++; | |
9f5a2895 | 329 | if (reply->keep_alive) |
1294c0fc | 330 | if (httpState->peer) |
331 | httpState->peer->stats.n_keepalives_recv++; | |
123abbe1 | 332 | ctx_exit(ctx); |
f5558c95 | 333 | } |
334 | } | |
335 | ||
603a02fd | 336 | static int |
337 | httpPconnTransferDone(HttpStateData * httpState) | |
338 | { | |
339 | /* return 1 if we got the last of the data on a persistent connection */ | |
340 | MemObject *mem = httpState->entry->mem_obj; | |
cb69b4c7 | 341 | HttpReply *reply = mem->reply; |
51fdcbd5 | 342 | debug(11, 3) ("httpPconnTransferDone: FD %d\n", httpState->fd); |
978e455f | 343 | /* |
99edd1c3 | 344 | * If we didn't send a keep-alive request header, then this |
978e455f | 345 | * can not be a persistent connection. |
346 | */ | |
b515fc11 | 347 | if (!httpState->flags.keepalive) |
603a02fd | 348 | return 0; |
9f5a2895 | 349 | /* |
350 | * What does the reply have to say about keep-alive? | |
351 | */ | |
352 | if (!reply->keep_alive) | |
353 | return 0; | |
51fdcbd5 | 354 | debug(11, 5) ("httpPconnTransferDone: content_length=%d\n", |
d8b249ef | 355 | reply->content_length); |
603a02fd | 356 | /* |
978e455f | 357 | * Deal with gross HTTP stuff |
358 | * - If we haven't seen the end of the reply headers, we can't | |
359 | * be persistent. | |
360 | * - For "200 OK" check the content-length in the next block. | |
978e455f | 361 | * - For "204 No Content" (even with content-length) we're done. |
362 | * - For "304 Not Modified" (even with content-length) we're done. | |
a3c60429 | 363 | * - 1XX replies never have a body; we're done. |
978e455f | 364 | * - For HEAD requests with content-length we're done. |
a3c60429 | 365 | * - For all other replies, check content length in next block. |
603a02fd | 366 | */ |
978e455f | 367 | if (httpState->reply_hdr_state < 2) |
368 | return 0; | |
cb69b4c7 | 369 | else if (reply->sline.status == HTTP_OK) |
a3c60429 | 370 | (void) 0; /* common case, continue */ |
cb69b4c7 | 371 | else if (reply->sline.status == HTTP_NO_CONTENT) |
978e455f | 372 | return 1; |
cb69b4c7 | 373 | else if (reply->sline.status == HTTP_NOT_MODIFIED) |
978e455f | 374 | return 1; |
cb69b4c7 | 375 | else if (reply->sline.status < HTTP_OK) |
a3c60429 | 376 | return 1; |
978e455f | 377 | else if (httpState->request->method == METHOD_HEAD) |
378 | return 1; | |
603a02fd | 379 | /* |
a3c60429 | 380 | * If there is no content-length, then we can't be |
978e455f | 381 | * persistent. If there is a content length, then we must |
382 | * wait until we've seen the end of the body. | |
603a02fd | 383 | */ |
d8b249ef | 384 | if (reply->content_length < 0) |
603a02fd | 385 | return 0; |
d8b249ef | 386 | else if (mem->inmem_hi < reply->content_length + reply->hdr_sz) |
603a02fd | 387 | return 0; |
978e455f | 388 | else |
b34ed725 | 389 | return 1; |
603a02fd | 390 | } |
090089c4 | 391 | |
392 | /* This will be called when data is ready to be read from fd. Read until | |
393 | * error or connection closed. */ | |
f5558c95 | 394 | /* XXX this function is too long! */ |
b8d8561b | 395 | static void |
b177367b | 396 | httpReadReply(int fd, void *data) |
090089c4 | 397 | { |
b177367b | 398 | HttpStateData *httpState = data; |
95d659f0 | 399 | LOCAL_ARRAY(char, buf, SQUID_TCP_SO_RCVBUF); |
bfcaf585 | 400 | StoreEntry *entry = httpState->entry; |
603a02fd | 401 | const request_t *request = httpState->request; |
090089c4 | 402 | int len; |
30a4f2a8 | 403 | int bin; |
090089c4 | 404 | int clen; |
447e176b | 405 | size_t read_sz; |
406 | #if DELAY_POOLS | |
407 | delay_id delay_id = delayMostBytesAllowed(entry->mem_obj); | |
408 | #endif | |
41462d93 | 409 | if (fwdAbortFetch(entry)) { |
9b312a19 | 410 | storeAbort(entry, 0); |
a3d5953d | 411 | comm_close(fd); |
412 | return; | |
234967c9 | 413 | } |
414 | /* check if we want to defer reading */ | |
1513873c | 415 | errno = 0; |
447e176b | 416 | read_sz = SQUID_TCP_SO_RCVBUF; |
417 | #if DELAY_POOLS | |
56e64999 | 418 | read_sz = delayBytesWanted(delay_id, 1, read_sz); |
447e176b | 419 | #endif |
886f2785 | 420 | Counter.syscalls.sock.reads++; |
447e176b | 421 | len = read(fd, buf, read_sz); |
a3d5953d | 422 | debug(11, 5) ("httpReadReply: FD %d: len %d.\n", fd, len); |
30a4f2a8 | 423 | if (len > 0) { |
ee1679df | 424 | fd_bytes(fd, len, FD_READ); |
447e176b | 425 | #if DELAY_POOLS |
426 | delayBytesIn(delay_id, len); | |
427 | #endif | |
a0f32775 | 428 | kb_incr(&Counter.server.all.kbytes_in, len); |
429 | kb_incr(&Counter.server.http.kbytes_in, len); | |
4f92c80c | 430 | commSetTimeout(fd, Config.Timeout.read, NULL, NULL); |
4a63c85f | 431 | IOStats.Http.reads++; |
30a4f2a8 | 432 | for (clen = len - 1, bin = 0; clen; bin++) |
433 | clen >>= 1; | |
434 | IOStats.Http.read_hist[bin]++; | |
435 | } | |
5ede6c8f | 436 | if (!httpState->reply_hdr && len > 0) { |
437 | /* Skip whitespace */ | |
438 | while (len > 0 && isspace(*buf)) | |
439 | xmemmove(buf, buf + 1, len--); | |
440 | if (len == 0) { | |
441 | /* Continue to read... */ | |
442 | commSetSelect(fd, COMM_SELECT_READ, httpReadReply, httpState, 0); | |
443 | return; | |
444 | } | |
445 | } | |
ba718c8f | 446 | if (len < 0) { |
55cb44f1 | 447 | debug(50, 2) ("httpReadReply: FD %d: read failure: %s.\n", |
448 | fd, xstrerror()); | |
b224ea98 | 449 | if (ignoreErrno(errno)) { |
9b312a19 | 450 | commSetSelect(fd, COMM_SELECT_READ, httpReadReply, httpState, 0); |
910169e5 | 451 | } else if (entry->mem_obj->inmem_hi == 0) { |
452 | fwdFail(httpState->fwdState, ERR_READ_ERROR, HTTP_INTERNAL_SERVER_ERROR, errno); | |
1afe05c5 | 453 | comm_close(fd); |
090089c4 | 454 | } else { |
55cb44f1 | 455 | storeAbort(entry, 0); |
0d4d4170 | 456 | comm_close(fd); |
090089c4 | 457 | } |
8350fe9b | 458 | } else if (len == 0 && entry->mem_obj->inmem_hi == 0) { |
910169e5 | 459 | fwdFail(httpState->fwdState, ERR_ZERO_SIZE_OBJECT, HTTP_SERVICE_UNAVAILABLE, errno); |
460 | httpState->eof = 1; | |
461 | comm_close(fd); | |
090089c4 | 462 | } else if (len == 0) { |
463 | /* Connection closed; retrieval done. */ | |
f86a6a46 | 464 | httpState->eof = 1; |
d1a43e28 | 465 | if (httpState->reply_hdr_state < 2) |
b34ed725 | 466 | /* |
467 | * Yes Henrik, there is a point to doing this. When we | |
468 | * called httpProcessReplyHeader() before, we didn't find | |
469 | * the end of headers, but now we are definately at EOF, so | |
470 | * we want to process the reply headers. | |
471 | */ | |
d1a43e28 | 472 | httpProcessReplyHeader(httpState, buf, len); |
7e3e1d01 | 473 | #ifdef PPNR_WIP |
474 | storePPNR(entry); | |
475 | #endif /* PPNR_WIP */ | |
d1a43e28 | 476 | storeComplete(entry); /* deallocates mem_obj->request */ |
0d4d4170 | 477 | comm_close(fd); |
090089c4 | 478 | } else { |
7e3e1d01 | 479 | #ifndef PPNR_WIP |
d1a43e28 | 480 | if (httpState->reply_hdr_state < 2) |
7e3e1d01 | 481 | #else |
482 | if (httpState->reply_hdr_state < 2) { | |
483 | #endif /* PPNR_WIP */ | |
30a4f2a8 | 484 | httpProcessReplyHeader(httpState, buf, len); |
7e3e1d01 | 485 | #ifdef PPNR_WIP |
486 | if (httpState->reply_hdr_state == 2) | |
e82d6d21 | 487 | storePPNR(entry); |
7e3e1d01 | 488 | } |
489 | #endif /* PPNR_WIP */ | |
620da955 | 490 | storeAppend(entry, buf, len); |
9d66d521 | 491 | #ifdef OPTIMISTIC_IO |
492 | if (entry->store_status == STORE_ABORTED) { | |
493 | /* | |
494 | * the above storeAppend() call could ABORT this entry, | |
495 | * in that case, the server FD should already be closed. | |
496 | * there's nothing for us to do. | |
497 | */ | |
498 | (void) 0; | |
499 | } else | |
500 | #endif | |
603a02fd | 501 | if (httpPconnTransferDone(httpState)) { |
5b29969a | 502 | /* yes we have to clear all these! */ |
8796b9e9 | 503 | commSetDefer(fd, NULL, NULL); |
5b29969a | 504 | commSetTimeout(fd, -1, NULL, NULL); |
505 | commSetSelect(fd, COMM_SELECT_READ, NULL, NULL, 0); | |
603a02fd | 506 | comm_remove_close_handler(fd, httpStateFree, httpState); |
507 | storeComplete(entry); /* deallocates mem_obj->request */ | |
52f0d243 | 508 | /* call storeComplete BEFORE fwdUnregister or else fwdUnregister |
509 | * will storeAbort */ | |
510 | fwdUnregister(fd, httpState->fwdState); | |
8796b9e9 | 511 | pconnPush(fd, request->host, request->port); |
603a02fd | 512 | httpState->fd = -1; |
513 | httpStateFree(-1, httpState); | |
514 | } else { | |
9f5a2895 | 515 | /* Wait for EOF condition */ |
603a02fd | 516 | commSetSelect(fd, COMM_SELECT_READ, httpReadReply, httpState, 0); |
517 | } | |
090089c4 | 518 | } |
519 | } | |
520 | ||
521 | /* This will be called when request write is complete. Schedule read of | |
522 | * reply. */ | |
b8d8561b | 523 | static void |
79a15e0a | 524 | httpSendComplete(int fd, char *bufnotused, size_t size, int errflag, void *data) |
090089c4 | 525 | { |
30a4f2a8 | 526 | HttpStateData *httpState = data; |
9b312a19 | 527 | StoreEntry *entry = httpState->entry; |
528 | ErrorState *err; | |
a3d5953d | 529 | debug(11, 5) ("httpSendComplete: FD %d: size %d: errflag %d.\n", |
090089c4 | 530 | fd, size, errflag); |
ee1679df | 531 | if (size > 0) { |
532 | fd_bytes(fd, size, FD_WRITE); | |
a0f32775 | 533 | kb_incr(&Counter.server.all.kbytes_out, size); |
399e85ea | 534 | kb_incr(&Counter.server.http.kbytes_out, size); |
ee1679df | 535 | } |
ea3a2a69 | 536 | if (errflag == COMM_ERR_CLOSING) |
537 | return; | |
090089c4 | 538 | if (errflag) { |
fe40a877 | 539 | err = errorCon(ERR_WRITE_ERROR, HTTP_INTERNAL_SERVER_ERROR); |
c45ed9ad | 540 | err->xerrno = errno; |
79a15e0a | 541 | err->request = requestLink(httpState->orig_request); |
9b312a19 | 542 | errorAppendEntry(entry, err); |
0d4d4170 | 543 | comm_close(fd); |
090089c4 | 544 | return; |
545 | } else { | |
546 | /* Schedule read reply. */ | |
b177367b | 547 | commSetSelect(fd, |
019dd986 | 548 | COMM_SELECT_READ, |
b177367b | 549 | httpReadReply, |
cd1fb0eb | 550 | httpState, 0); |
41462d93 | 551 | commSetDefer(fd, fwdCheckDeferRead, entry); |
090089c4 | 552 | } |
553 | } | |
554 | ||
99edd1c3 | 555 | /* |
556 | * build request headers and append them to a given MemBuf | |
557 | * used by httpBuildRequestPrefix() | |
558 | * note: calls httpHeaderInit(), the caller is responsible for Clean()-ing | |
559 | */ | |
e1e72f06 | 560 | void |
6bf8443a | 561 | httpBuildRequestHeader(request_t * request, |
562 | request_t * orig_request, | |
563 | StoreEntry * entry, | |
5999b776 | 564 | HttpHeader * hdr_out, |
603a02fd | 565 | int cfd, |
b515fc11 | 566 | http_state_flags flags) |
6bf8443a | 567 | { |
99edd1c3 | 568 | /* building buffer for complex strings */ |
5999b776 | 569 | #define BBUF_SZ (MAX_URL+32) |
99edd1c3 | 570 | LOCAL_ARRAY(char, bbuf, BBUF_SZ); |
571 | String strConnection = StringNull; | |
572 | const HttpHeader *hdr_in = &orig_request->header; | |
d192d11f | 573 | int filter_range; |
99edd1c3 | 574 | const HttpHeaderEntry *e; |
575 | HttpHeaderPos pos = HttpHeaderInitPos; | |
2246b732 | 576 | httpHeaderInit(hdr_out, hoRequest); |
99edd1c3 | 577 | /* append our IMS header */ |
e17dc75c | 578 | if (entry && entry->lastmod > -1 && request->method == METHOD_GET) |
99edd1c3 | 579 | httpHeaderPutTime(hdr_out, HDR_IF_MODIFIED_SINCE, entry->lastmod); |
580 | ||
137ee196 | 581 | /* decide if we want to filter out Range specs |
582 | * no reason to filter out if the reply will not be cachable | |
583 | * or if we cannot parse the specs */ | |
d192d11f | 584 | filter_range = |
92695e5e | 585 | orig_request->range && orig_request->flags.cachable; |
137ee196 | 586 | |
99edd1c3 | 587 | strConnection = httpHeaderGetList(hdr_in, HDR_CONNECTION); |
588 | while ((e = httpHeaderGetEntry(hdr_in, &pos))) { | |
589 | debug(11, 5) ("httpBuildRequestHeader: %s: %s\n", | |
590 | strBuf(e->name), strBuf(e->value)); | |
591 | if (!httpRequestHdrAllowed(e, &strConnection)) | |
6bf8443a | 592 | continue; |
99edd1c3 | 593 | switch (e->id) { |
594 | case HDR_PROXY_AUTHORIZATION: | |
afe95a7e | 595 | /* If we're not going to do proxy auth, then it must be passed on */ |
92695e5e | 596 | if (!request->flags.used_proxy_auth) |
99edd1c3 | 597 | httpHeaderAddEntry(hdr_out, httpHeaderEntryClone(e)); |
598 | break; | |
599 | case HDR_HOST: | |
77ed547a | 600 | /* Don't use client's Host: header for redirected requests */ |
92695e5e | 601 | if (!request->flags.redirected) |
99edd1c3 | 602 | httpHeaderAddEntry(hdr_out, httpHeaderEntryClone(e)); |
603 | break; | |
604 | case HDR_IF_MODIFIED_SINCE: | |
605 | /* append unless we added our own; | |
606 | * note: at most one client's ims header can pass through */ | |
607 | if (!httpHeaderHas(hdr_out, HDR_IF_MODIFIED_SINCE)) | |
608 | httpHeaderAddEntry(hdr_out, httpHeaderEntryClone(e)); | |
609 | break; | |
610 | case HDR_MAX_FORWARDS: | |
b3b64e58 | 611 | if (orig_request->method == METHOD_TRACE) { |
99edd1c3 | 612 | /* sacrificing efficiency over clarity, etc. */ |
613 | const int hops = httpHeaderGetInt(hdr_in, HDR_MAX_FORWARDS); | |
614 | if (hops > 0) | |
5999b776 | 615 | httpHeaderPutInt(hdr_out, HDR_MAX_FORWARDS, hops - 1); |
b3b64e58 | 616 | } |
99edd1c3 | 617 | break; |
137ee196 | 618 | case HDR_RANGE: |
a9771e51 | 619 | case HDR_IF_RANGE: |
d192d11f | 620 | if (!filter_range) |
137ee196 | 621 | httpHeaderAddEntry(hdr_out, httpHeaderEntryClone(e)); |
622 | break; | |
99edd1c3 | 623 | case HDR_PROXY_CONNECTION: |
624 | case HDR_CONNECTION: | |
625 | case HDR_VIA: | |
626 | case HDR_X_FORWARDED_FOR: | |
627 | case HDR_CACHE_CONTROL: | |
628 | /* append these after the loop if needed */ | |
629 | break; | |
630 | default: | |
631 | /* pass on all other header fields */ | |
632 | httpHeaderAddEntry(hdr_out, httpHeaderEntryClone(e)); | |
66f7337b | 633 | } |
88738790 | 634 | } |
99edd1c3 | 635 | |
636 | /* append fake user agent if configured and | |
637 | * the real one is not supplied by the client */ | |
638 | if (Config.fake_ua && !httpHeaderHas(hdr_out, HDR_USER_AGENT)) | |
639 | httpHeaderPutStr(hdr_out, HDR_USER_AGENT, Config.fake_ua); | |
640 | ||
641 | /* append Via */ | |
642 | { | |
643 | String strVia = httpHeaderGetList(hdr_in, HDR_VIA); | |
644 | snprintf(bbuf, BBUF_SZ, "%3.1f %s", orig_request->http_ver, ThisCache); | |
645 | strListAdd(&strVia, bbuf, ','); | |
646 | httpHeaderPutStr(hdr_out, HDR_VIA, strBuf(strVia)); | |
647 | stringClean(&strVia); | |
6bf8443a | 648 | } |
99edd1c3 | 649 | /* append X-Forwarded-For */ |
650 | { | |
651 | String strFwd = httpHeaderGetList(hdr_in, HDR_X_FORWARDED_FOR); | |
652 | strListAdd(&strFwd, (cfd < 0 ? "unknown" : fd_table[cfd].ipaddr), ','); | |
653 | httpHeaderPutStr(hdr_out, HDR_X_FORWARDED_FOR, strBuf(strFwd)); | |
654 | stringClean(&strFwd); | |
655 | } | |
656 | /* append Host if not there already */ | |
657 | if (!httpHeaderHas(hdr_out, HDR_HOST)) { | |
658 | /* use port# only if not default */ | |
659 | if (orig_request->port == urlDefaultPort(orig_request->protocol)) { | |
660 | httpHeaderPutStr(hdr_out, HDR_HOST, orig_request->host); | |
661 | } else { | |
2246b732 | 662 | httpHeaderPutStrf(hdr_out, HDR_HOST, "%s:%d", |
99edd1c3 | 663 | orig_request->host, (int) orig_request->port); |
99edd1c3 | 664 | } |
6bf8443a | 665 | } |
99edd1c3 | 666 | /* append Cache-Control, add max-age if not there already */ |
667 | { | |
668 | HttpHdrCc *cc = httpHeaderGetCc(hdr_in); | |
669 | if (!cc) | |
670 | cc = httpHdrCcCreate(); | |
671 | if (!EBIT_TEST(cc->mask, CC_MAX_AGE)) { | |
9b5d1d21 | 672 | const char *url = entry ? storeUrl(entry) : urlCanonical(orig_request); |
99edd1c3 | 673 | httpHdrCcSetMaxAge(cc, getMaxAge(url)); |
674 | if (strLen(request->urlpath)) | |
675 | assert(strstr(url, strBuf(request->urlpath))); | |
676 | } | |
677 | httpHeaderPutCc(hdr_out, cc); | |
678 | httpHdrCcDestroy(cc); | |
6bf8443a | 679 | } |
99edd1c3 | 680 | /* maybe append Connection: keep-alive */ |
b515fc11 | 681 | if (flags.keepalive) { |
682 | if (flags.proxying) { | |
99edd1c3 | 683 | httpHeaderPutStr(hdr_out, HDR_PROXY_CONNECTION, "keep-alive"); |
603a02fd | 684 | } else { |
99edd1c3 | 685 | httpHeaderPutStr(hdr_out, HDR_CONNECTION, "keep-alive"); |
603a02fd | 686 | } |
603a02fd | 687 | } |
99edd1c3 | 688 | stringClean(&strConnection); |
689 | } | |
690 | ||
691 | /* build request prefix and append it to a given MemBuf; | |
692 | * return the length of the prefix */ | |
693 | size_t | |
694 | httpBuildRequestPrefix(request_t * request, | |
695 | request_t * orig_request, | |
696 | StoreEntry * entry, | |
5999b776 | 697 | MemBuf * mb, |
99edd1c3 | 698 | int cfd, |
b515fc11 | 699 | http_state_flags flags) |
99edd1c3 | 700 | { |
701 | const int offset = mb->size; | |
702 | memBufPrintf(mb, "%s %s HTTP/1.0\r\n", | |
703 | RequestMethodStr[request->method], | |
704 | strLen(request->urlpath) ? strBuf(request->urlpath) : "/"); | |
705 | /* build and pack headers */ | |
706 | { | |
707 | HttpHeader hdr; | |
708 | Packer p; | |
709 | httpBuildRequestHeader(request, orig_request, entry, &hdr, cfd, flags); | |
710 | packerToMemInit(&p, mb); | |
711 | httpHeaderPackInto(&hdr, &p); | |
712 | httpHeaderClean(&hdr); | |
713 | packerClean(&p); | |
9d9d144b | 714 | } |
99edd1c3 | 715 | /* append header terminator */ |
716 | memBufAppend(mb, "\r\n", 2); | |
717 | return mb->size - offset; | |
6bf8443a | 718 | } |
719 | ||
090089c4 | 720 | /* This will be called when connect completes. Write request. */ |
b8d8561b | 721 | static void |
b177367b | 722 | httpSendRequest(int fd, void *data) |
090089c4 | 723 | { |
b177367b | 724 | HttpStateData *httpState = data; |
99edd1c3 | 725 | MemBuf mb; |
30a4f2a8 | 726 | request_t *req = httpState->request; |
620da955 | 727 | StoreEntry *entry = httpState->entry; |
2a26c096 | 728 | int cfd; |
1294c0fc | 729 | peer *p = httpState->peer; |
901e234d | 730 | CWCB *sendHeaderDone; |
090089c4 | 731 | |
a3d5953d | 732 | debug(11, 5) ("httpSendRequest: FD %d: httpState %p.\n", fd, httpState); |
090089c4 | 733 | |
efb9218c | 734 | if (pumpMethod(req->method)) |
7db8b16d | 735 | sendHeaderDone = httpSendRequestEntry; |
736 | else | |
737 | sendHeaderDone = httpSendComplete; | |
54220df8 | 738 | |
2a26c096 | 739 | if (!opt_forwarded_for) |
6bf8443a | 740 | cfd = -1; |
2a26c096 | 741 | else if (entry->mem_obj == NULL) |
6bf8443a | 742 | cfd = -1; |
2a26c096 | 743 | else |
382d851a | 744 | cfd = entry->mem_obj->fd; |
b0a1e5bf | 745 | assert(-1 == cfd || FD_SOCKET == fd_table[cfd].type); |
1294c0fc | 746 | if (p != NULL) |
b515fc11 | 747 | httpState->flags.proxying = 1; |
efb9218c | 748 | /* |
99edd1c3 | 749 | * Is keep-alive okay for all request methods? |
efb9218c | 750 | */ |
751 | if (p == NULL) | |
b515fc11 | 752 | httpState->flags.keepalive = 1; |
efb9218c | 753 | else if (p->stats.n_keepalives_sent < 10) |
b515fc11 | 754 | httpState->flags.keepalive = 1; |
efb9218c | 755 | else if ((double) p->stats.n_keepalives_recv / (double) p->stats.n_keepalives_sent > 0.50) |
b515fc11 | 756 | httpState->flags.keepalive = 1; |
99edd1c3 | 757 | memBufDefInit(&mb); |
758 | httpBuildRequestPrefix(req, | |
79a15e0a | 759 | httpState->orig_request, |
6bf8443a | 760 | entry, |
99edd1c3 | 761 | &mb, |
603a02fd | 762 | cfd, |
763 | httpState->flags); | |
99edd1c3 | 764 | debug(11, 6) ("httpSendRequest: FD %d:\n%s\n", fd, mb.buf); |
765 | comm_write_mbuf(fd, mb, sendHeaderDone, httpState); | |
090089c4 | 766 | } |
767 | ||
910169e5 | 768 | void |
769 | httpStart(FwdState * fwdState, int fd) | |
603a02fd | 770 | { |
cb87dab6 | 771 | HttpStateData *httpState = memAllocate(MEM_HTTP_STATE_DATA); |
910169e5 | 772 | request_t *proxy_req; |
773 | request_t *orig_req = fwdState->request; | |
774 | debug(11, 3) ("httpStart: \"%s %s\"\n", | |
775 | RequestMethodStr[orig_req->method], | |
776 | storeUrl(fwdState->entry)); | |
cb87dab6 | 777 | cbdataAdd(httpState, MEM_HTTP_STATE_DATA); |
910169e5 | 778 | storeLockObject(fwdState->entry); |
779 | httpState->fwdState = fwdState; | |
780 | httpState->entry = fwdState->entry; | |
9e4ad609 | 781 | httpState->fd = fd; |
910169e5 | 782 | if (fwdState->servers) |
783 | httpState->peer = fwdState->servers->peer; /* might be NULL */ | |
784 | if (httpState->peer) { | |
785 | proxy_req = requestCreate(orig_req->method, | |
786 | PROTO_NONE, storeUrl(httpState->entry)); | |
787 | xstrncpy(proxy_req->host, httpState->peer->host, SQUIDHOSTNAMELEN); | |
788 | proxy_req->port = httpState->peer->http_port; | |
23e8446b | 789 | proxy_req->flags = orig_req->flags; |
910169e5 | 790 | httpState->request = requestLink(proxy_req); |
910169e5 | 791 | httpState->orig_request = requestLink(orig_req); |
92695e5e | 792 | proxy_req->flags.proxying = 1; |
910169e5 | 793 | /* |
794 | * This NEIGHBOR_PROXY_ONLY check probably shouldn't be here. | |
795 | * We might end up getting the object from somewhere else if, | |
796 | * for example, the request to this neighbor fails. | |
797 | */ | |
cd196bc8 | 798 | if (httpState->peer->options.proxy_only) |
910169e5 | 799 | storeReleaseRequest(httpState->entry); |
95e36d02 | 800 | #if DELAY_POOLS |
cd196bc8 | 801 | if (httpState->peer->options.no_delay) { |
447e176b | 802 | proxy_req->delay_id = 0; |
95e36d02 | 803 | } else { |
447e176b | 804 | proxy_req->delay_id = orig_req->delay_id; |
95e36d02 | 805 | } |
806 | #endif | |
603a02fd | 807 | } else { |
910169e5 | 808 | httpState->request = requestLink(orig_req); |
809 | httpState->orig_request = requestLink(orig_req); | |
603a02fd | 810 | } |
910169e5 | 811 | /* |
812 | * register the handler to free HTTP state data when the FD closes | |
813 | */ | |
814 | comm_add_close_handler(fd, httpStateFree, httpState); | |
a0f32775 | 815 | Counter.server.all.requests++; |
816 | Counter.server.http.requests++; | |
41462d93 | 817 | httpConnectDone(fd, COMM_OK, httpState); |
e5f6c5c2 | 818 | } |
819 | ||
820 | static void | |
821 | httpConnectDone(int fd, int status, void *data) | |
822 | { | |
823 | HttpStateData *httpState = data; | |
824 | request_t *request = httpState->request; | |
825 | StoreEntry *entry = httpState->entry; | |
9b312a19 | 826 | ErrorState *err; |
edeb28fd | 827 | if (status == COMM_ERR_DNS) { |
a3d5953d | 828 | debug(11, 4) ("httpConnectDone: Unknown host: %s\n", request->host); |
fe40a877 | 829 | err = errorCon(ERR_DNS_FAIL, HTTP_SERVICE_UNAVAILABLE); |
9b312a19 | 830 | err->dnsserver_msg = xstrdup(dns_error_message); |
79a15e0a | 831 | err->request = requestLink(httpState->orig_request); |
9b312a19 | 832 | errorAppendEntry(entry, err); |
edeb28fd | 833 | comm_close(fd); |
834 | } else if (status != COMM_OK) { | |
fe40a877 | 835 | err = errorCon(ERR_CONNECT_FAIL, HTTP_SERVICE_UNAVAILABLE); |
c45ed9ad | 836 | err->xerrno = errno; |
9b312a19 | 837 | err->host = xstrdup(request->host); |
838 | err->port = request->port; | |
79a15e0a | 839 | err->request = requestLink(httpState->orig_request); |
9b312a19 | 840 | errorAppendEntry(entry, err); |
1294c0fc | 841 | if (httpState->peer) |
842 | peerCheckConnectStart(httpState->peer); | |
e5f6c5c2 | 843 | comm_close(fd); |
844 | } else { | |
bfcaf585 | 845 | commSetSelect(fd, COMM_SELECT_WRITE, httpSendRequest, httpState, 0); |
86cf9987 | 846 | commSetTimeout(fd, Config.Timeout.read, httpTimeout, httpState); |
090089c4 | 847 | } |
090089c4 | 848 | } |
849 | ||
54220df8 | 850 | static void |
7db8b16d | 851 | httpSendRequestEntry(int fd, char *bufnotused, size_t size, int errflag, void *data) |
54220df8 | 852 | { |
853 | HttpStateData *httpState = data; | |
854 | StoreEntry *entry = httpState->entry; | |
855 | ErrorState *err; | |
856 | debug(11, 5) ("httpSendRequestEntry: FD %d: size %d: errflag %d.\n", | |
7db8b16d | 857 | fd, size, errflag); |
54220df8 | 858 | if (size > 0) { |
7db8b16d | 859 | fd_bytes(fd, size, FD_WRITE); |
54220df8 | 860 | kb_incr(&Counter.server.all.kbytes_out, size); |
861 | kb_incr(&Counter.server.http.kbytes_out, size); | |
862 | } | |
863 | if (errflag == COMM_ERR_CLOSING) | |
7db8b16d | 864 | return; |
54220df8 | 865 | if (errflag) { |
7db8b16d | 866 | err = errorCon(ERR_WRITE_ERROR, HTTP_INTERNAL_SERVER_ERROR); |
867 | err->xerrno = errno; | |
868 | err->request = requestLink(httpState->orig_request); | |
869 | errorAppendEntry(entry, err); | |
870 | comm_close(fd); | |
871 | return; | |
54220df8 | 872 | } |
7db8b16d | 873 | pumpStart(fd, entry, httpState->orig_request, httpSendComplete, httpState); |
54220df8 | 874 | } |