UNIXworkcode

1 /* 2 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. 3 * 4 * Copyright 2013 Olaf Wintermann. All rights reserved. 5 * 6 * Redistribution and use in source and binary forms, with or without 7 * modification, are permitted provided that the following conditions are met: 8 * 9 * 1. Redistributions of source code must retain the above copyright 10 * notice, this list of conditions and the following disclaimer. 11 * 12 * 2. Redistributions in binary form must reproduce the above copyright 13 * notice, this list of conditions and the following disclaimer in the 14 * documentation and/or other materials provided with the distribution. 15 * 16 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 19 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 20 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 21 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 22 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 23 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 24 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 25 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 26 * POSSIBILITY OF SUCH DAMAGE. 27 */ 28 29 #include <stdio.h> 30 #include <sys/file.h> 31 #include <sys/stat.h> 32 33 #include "service.h" 34 #include "../util/io.h" 35 #include "../util/pblock.h" 36 #include "../util/util.h" 37 #include "../daemon/protocol.h" 38 #include "../daemon/vfs.h" 39 40 #include "../util/strbuf.h" 41 #include <ucx/string.h> 42 #include <ucx/utils.h> 43 44 #include <errno.h> 45 46 /* 47 * prepares servicing a file 48 * 49 * adds content-length header 50 * 51 * return the opened file 52 */ 53 SYS_FILE prepare_service_file(Session *sn, Request *rq, VFSContext *vfs, struct stat *s) { 54 char *path = pblock_findkeyval(pb_key_path, rq->vars); 55 56 // open the file 57 SYS_FILE fd = vfs_open(vfs, path, O_RDONLY); 58 if(!fd) { 59 // vfs_open sets http status code 60 return NULL; 61 } 62 63 // get stat 64 if(vfs_fstat(vfs, fd, s) != 0) { 65 //perror("prepare_service_file: stat"); 66 protocol_status(sn, rq, 500, NULL); 67 return NULL; 68 } 69 70 // check if the file is a directory 71 if(S_ISDIR(s->st_mode)) { 72 pblock_nvinsert("content-length", "0", rq->srvhdrs); 73 pblock_removekey(pb_key_content_type, rq->srvhdrs); 74 char *uri = pblock_findkeyval(pb_key_uri, rq->reqpb); 75 size_t urilen = strlen(uri); 76 char *location = pool_malloc(sn->pool, urilen + 2); 77 memcpy(location, uri, urilen); 78 location[urilen] = '/'; 79 location[urilen+1] = '\0'; 80 pblock_kvinsert(pb_key_location, location, urilen + 1, rq->srvhdrs); 81 protocol_status(sn, rq, 302, NULL); 82 http_start_response(sn, rq); 83 vfs_close(fd); 84 return NULL; 85 } 86 87 // sets last-modified, content-length and checks conditions 88 if(http_set_finfo(sn, rq, s) != REQ_PROCEED) { 89 vfs_close(fd); 90 return NULL; 91 } 92 93 // TODO: check if vfs can seek 94 pblock_kvinsert(pb_key_accept_ranges, "bytes", 5, rq->srvhdrs); 95 96 // start response 97 protocol_status(sn, rq, 200, NULL); 98 99 return fd; 100 } 101 102 static void free_range(Session *sn, HttpRange *range) { 103 HttpRange *elm = range; 104 while(elm) { 105 HttpRange *next = elm->next; 106 pool_free(sn->pool, elm); 107 elm = next; 108 } 109 } 110 111 static HttpRange* parse_range(Session *sn, char *header, int *status) { 112 *status = PROTOCOL_OK; 113 114 sstr_t range = sstrtrim(sstr(header)); 115 if(!sstrprefix(range, S("bytes="))) { 116 // unknown range unit - ignore range header 117 return NULL; 118 } 119 120 // get byte-range-set 121 range = sstrsubs(range, 6); 122 if(range.length < 1) { 123 return NULL; 124 } 125 126 HttpRange *range_list = NULL; 127 HttpRange *last = NULL; 128 off_t begin = -1; 129 int start = 0; 130 int hasbegin = 0; 131 for(int i=0;i<=range.length;i++) { 132 char c = range.ptr[i]; 133 if(c == '-') { 134 sstr_t num = sstrsubsl(range, start, i-start); 135 if(num.length == 0) { 136 // empty string before '-' is legal 137 hasbegin = 1; 138 begin = -1; 139 start = i+1; 140 continue; 141 } 142 char *end; 143 long long n = strtoll(num.ptr, &end, 10); 144 if(errno == 0 && end == range.ptr + i && n >= 0) { 145 begin = n; 146 hasbegin = 1; 147 start = i+1; 148 } else { 149 // syntax error 150 free_range(sn, range_list); 151 return NULL; 152 } 153 } else if(c == ',' || c == '\0') { 154 sstr_t num = sstrsubsl(range, start, i-start); 155 if(hasbegin) { 156 long long n; 157 if(num.length == 0) { 158 // empty string after '-' is legal 159 n = -1; 160 } else { 161 char *end; 162 n = strtoll(num.ptr, &end, 10); 163 if(errno != 0 || end != range.ptr + i || n < 0) { 164 // syntax error 165 free_range(sn, range_list); 166 return NULL; 167 } 168 } 169 170 if(!(begin < 0 && n < 0)) { 171 // range: begin - n 172 HttpRange *rangeelm = pool_malloc(sn->pool, sizeof(HttpRange)); 173 if(!rangeelm) { 174 free_range(sn, range_list); 175 *status = PROTOCOL_SERVER_ERROR; 176 return NULL; 177 } 178 rangeelm->begin = begin; 179 rangeelm->end = n; 180 rangeelm->next = NULL; 181 if(!last) { 182 range_list = rangeelm; 183 last = rangeelm; 184 } else { 185 last->next = rangeelm; 186 last = rangeelm; 187 } 188 189 hasbegin = 0; 190 start = i+1; 191 continue; 192 } 193 } 194 195 // syntax error 196 free_range(sn, range_list); 197 return NULL; 198 } 199 } 200 201 return range_list; 202 } 203 204 static int validate_range(HttpRange *range, struct stat *finfo, int *status) { 205 off_t max_len = finfo->st_size; 206 while(range) { 207 if(range->begin > 0 && range->end > 0) { 208 if(range->end < range->begin) { 209 *status = PROTOCOL_REQUESTED_RANGE_NOT_SATISFIABLE; 210 return 0; 211 } 212 } 213 if(range->begin >= max_len) { 214 *status = PROTOCOL_REQUESTED_RANGE_NOT_SATISFIABLE; 215 return 0; 216 } 217 if(range->end >= max_len) { 218 *status = PROTOCOL_REQUESTED_RANGE_NOT_SATISFIABLE; 219 return 0; 220 } 221 222 range = range->next; 223 } 224 225 // TODO: check for Denial-of-Service Attacks 226 227 return 1; 228 } 229 230 /* 231 * translates a HttpRange element to a begin offset and a length 232 * the HttpRange must be validated 233 */ 234 static void range2off(HttpRange *range, off_t filelen, off_t *begin, off_t *length) { 235 if(range->begin < 0) { 236 // bytes=-a 237 *begin = filelen - range->end; 238 *length = range->end; 239 } else if(range->end < 0) { 240 // bytes=a- 241 *begin = range->begin; 242 *length = filelen - range->begin; 243 } else { 244 // bytes=a-b 245 *begin = range->begin; 246 *length = range->end + 1 - range->begin; 247 } 248 } 249 250 #define SF_MAX_LEN 0x8000000 251 252 static int send_range(Session *sn, SYS_FILE fd, off_t offset, off_t length, char *header, int headerlen) { 253 off_t remaining = length; 254 255 sendfiledata sfd; 256 sfd.fd = fd; 257 sfd.header = header; 258 sfd.hlen = headerlen; 259 sfd.trailer = NULL; 260 261 while(remaining > 0) { 262 size_t sflen = remaining < SF_MAX_LEN ? remaining : SF_MAX_LEN; 263 sfd.offset = offset; 264 sfd.len = sflen; 265 266 ssize_t r = net_sendfile(sn->csd, &sfd); 267 if(r < 0) { 268 return -1; 269 } 270 271 sfd.header = NULL; // make sure the header is only sent once 272 offset += r; 273 remaining -= r; 274 } 275 276 return 0; 277 } 278 279 280 static void send_range_cleanup(AsyncSendRange *asr) { 281 WSBool error = asr->error; 282 Session *sn = asr->sn; 283 Request *rq = asr->rq; 284 285 pool_handle_t *pool = asr->sn->pool; 286 vfs_close(asr->in); 287 pool_free(pool, asr->aio->buf); 288 pool_free(pool, asr->aio); 289 pool_free(pool, asr->readev); 290 pool_free(pool, asr->writeev); 291 pool_free(pool, asr); 292 293 int ret = REQ_PROCEED; 294 if(error) { 295 rq->rq_attr.keep_alive = 0; 296 ret = REQ_ABORTED; 297 } 298 // return to nsapi loop 299 nsapi_function_return(sn, rq, ret); 300 } 301 302 static int send_buf( 303 SYS_NETFD out, 304 char *restrict buf, 305 size_t len, 306 size_t *restrict pos) 307 { 308 while(*pos < len) { 309 ssize_t w = net_write(out, buf + *pos, len - *pos); 310 if(w <= 0) { 311 return -1; 312 } 313 *pos += w; 314 } 315 return 0; 316 } 317 318 static int send_bytes(AsyncSendRange *asr, WSBool *completed) { 319 *completed = FALSE; 320 if(asr->header) { 321 if(send_buf(asr->out, asr->header, asr->headerlen, &asr->headerpos)) { 322 if(net_errno(asr->out) == EAGAIN) { 323 return 0; 324 } else { 325 asr->error = TRUE; 326 return 1; 327 } 328 } 329 if(asr->headerpos >= asr->headerlen) { 330 asr->header = NULL; 331 } 332 } 333 334 if(send_buf(asr->out, asr->aio->buf, asr->aio->result, &asr->wpos)) { 335 if(net_errno(asr->out) == EAGAIN) { 336 return 0; 337 } else { 338 asr->error = TRUE; 339 return 1; 340 } 341 } 342 343 if(!asr->read_complete) { 344 // write completed => new asynchronous read 345 asr->aio->offset += asr->aio->result; 346 size_t length = asr->end - asr->offset; 347 asr->aio->nbytes = AIO_BUF_SIZE < length ? AIO_BUF_SIZE : length; 348 asr->read_inprogress = TRUE; 349 if(system_aio_read(asr->aio)) { 350 asr->error = TRUE; 351 return 1; 352 } 353 } 354 *completed = TRUE; 355 return 0; 356 } 357 358 static int send_range_readevent(EventHandler *ev, Event *event) { 359 AsyncSendRange *asr = event->cookie; 360 asr->read_inprogress = FALSE; 361 asr->wpos = 0; 362 asr->offset += asr->aio->result; 363 if(asr->error || asr->aio->result < 0) { 364 return 0; 365 } 366 367 int ret = 1; 368 if(asr->aio->result == 0 || asr->offset >= asr->end) { 369 asr->read_complete = TRUE; 370 ret = 0; 371 } 372 373 WSBool completed; 374 if(send_bytes(asr, &completed)) { 375 return 0; 376 } 377 if(!completed && !asr->write_inprogress) { 378 asr->write_inprogress = TRUE; 379 if(event_pollout(ev, asr->out, asr->writeev)) { 380 asr->error = TRUE; 381 return 0; 382 } 383 } 384 385 return ret; 386 } 387 388 static int send_range_writeevent(EventHandler *ev, Event *event) { 389 AsyncSendRange *asr = event->cookie; 390 if(asr->error) { 391 return 1; 392 } 393 394 WSBool completed; 395 if(send_bytes(asr, &completed)) { 396 return 1; 397 } 398 399 if(completed) { 400 return 0; 401 } 402 403 return 1; 404 } 405 406 static int send_range_aio_finish(EventHandler *ev, Event *event) { 407 AsyncSendRange *asr = event->cookie; 408 if(!asr->write_inprogress) { 409 send_range_cleanup(asr); 410 } 411 asr->read_inprogress = FALSE; 412 return 0; 413 } 414 415 static int send_range_poll_finish(EventHandler *ev, Event *event) { 416 AsyncSendRange *asr = event->cookie; 417 if(!asr->read_inprogress) { 418 send_range_cleanup(asr); 419 } 420 asr->write_inprogress = FALSE; 421 return 0; 422 } 423 424 static int send_range_aio(Session *sn, Request *rq, SYS_FILE fd, off_t offset, off_t length, char *header, int headerlen) { 425 net_setnonblock(sn->csd, TRUE); 426 427 // try to send the header 428 ssize_t hw = net_write(sn->csd, header, headerlen); 429 if(hw < 0) { 430 if(net_errno(sn->csd) == EAGAIN) { 431 hw = 0; 432 } else { 433 return REQ_ABORTED; 434 } 435 } 436 437 AsyncSendRange *asr = pool_malloc(sn->pool, sizeof(AsyncSendRange)); 438 asr->sn = sn; 439 asr->rq = rq; 440 asr->in = fd; 441 asr->out = sn->csd; 442 asr->offset = offset; 443 asr->end = offset + length; 444 //asr->length = length; 445 asr->pos = offset; 446 asr->read_complete = FALSE; 447 asr->read_inprogress = FALSE; 448 asr->write_inprogress = FALSE; 449 asr->error = FALSE; 450 if(hw == headerlen) { 451 asr->header = NULL; 452 asr->headerlen = 0; 453 asr->headerpos = 0; 454 } else { 455 asr->header = header; 456 asr->headerlen = headerlen; 457 asr->headerpos = hw; 458 } 459 460 Event *readev = pool_malloc(sn->pool, sizeof(Event)); 461 ZERO(readev, sizeof(Event)); 462 readev->cookie = asr; 463 readev->fn = send_range_readevent; 464 readev->finish = send_range_aio_finish; 465 466 Event *writeev = pool_malloc(sn->pool, sizeof(Event)); 467 ZERO(writeev, sizeof(Event)); 468 writeev->cookie = asr; 469 writeev->fn = send_range_writeevent; 470 writeev->finish = send_range_poll_finish; 471 472 asr->readev = readev; 473 asr->writeev = writeev; 474 475 aiocb_s *aio = pool_malloc(sn->pool, sizeof(aiocb_s)); 476 aio->buf = pool_malloc(sn->pool, AIO_BUF_SIZE); 477 aio->nbytes = AIO_BUF_SIZE < length ? AIO_BUF_SIZE : length; 478 aio->filedes = fd; 479 aio->offset = offset; 480 aio->evhandler = sn->ev; 481 aio->event = readev; 482 483 asr->aio = aio; 484 asr->wpos = 0; 485 486 asr->read_inprogress = TRUE; 487 if(system_aio_read(aio)) { 488 send_range_cleanup(asr); 489 return REQ_ABORTED; 490 } 491 asr->read_inprogress = TRUE; 492 493 return REQ_PROCESSING; 494 } 495 496 struct multi_range_elm { 497 sstr_t header; 498 off_t offset; 499 off_t length; 500 }; 501 502 static int send_multi_range(Session *sn, Request *rq, SYS_FILE fd, off_t filelen, HttpRange *range) { 503 pb_param *content_type = pblock_remove("content-type", rq->srvhdrs); 504 505 char sep[64]; 506 int seplen = util_mime_separator(sep); 507 508 sstr_t newct = ucx_sprintf("multipart/byteranges; boundary=%s", sep+4); 509 pblock_kvinsert( 510 pb_key_content_type, 511 newct.ptr, 512 newct.length, 513 rq->srvhdrs); 514 free(newct.ptr); 515 516 // calculate content-length 517 off_t response_len = 0; 518 519 int nrange = 0; 520 HttpRange *rangeelm = range; 521 while(rangeelm) { 522 nrange++; 523 rangeelm = rangeelm->next; 524 } 525 526 struct multi_range_elm *r = calloc(nrange, sizeof(struct multi_range_elm)); 527 rangeelm = range; 528 int i=0; 529 while(rangeelm) { 530 range2off(rangeelm, filelen, &(r[i].offset), &(r[i].length)); 531 r[i].header = ucx_sprintf( 532 "%s\r\nContent-Type: %s\r\nContent-Range: bytes %lld-%lld/%lld\r\n\r\n", 533 sep, 534 content_type->value, 535 (long long)r[i].offset, 536 (long long)r[i].offset+r[i].length - 1, 537 filelen); 538 539 response_len += r[i].header.length + r[i].length; 540 541 rangeelm = rangeelm->next; 542 i++; 543 } 544 545 response_len += seplen + 4; // trailer: sep + '--' + CRLF 546 547 // finally, set the content-length header 548 pblock_kllinsert( 549 pb_key_content_length, 550 (long long)response_len, 551 rq->srvhdrs); 552 553 // and start the response 554 http_start_response(sn, rq); 555 556 rangeelm = range; 557 i = 0; 558 while(rangeelm) { 559 if(send_range(sn, fd, r[i].offset, r[i].length, r[i].header.ptr, r[i].header.length)) { 560 // TODO: error 561 } 562 rangeelm = rangeelm->next; 563 i++; 564 } 565 net_printf(sn->csd, "%s--\r\n", sep); 566 567 free(r); 568 return 0; 569 } 570 571 int send_file(pblock *pb, Session *sn, Request *rq) { 572 struct stat s; 573 VFSContext *vfs = vfs_request_context(sn, rq); 574 SYS_FILE fd = prepare_service_file(sn, rq, vfs, &s); 575 if(!fd) { 576 // if an error occurs, prepare_service_file sets the http status code 577 // we can just return REQ_ABORTED 578 return REQ_ABORTED; 579 } 580 581 // get and validate range header 582 char *range_header = pblock_findkeyval(pb_key_range, rq->headers); 583 HttpRange *range = NULL; 584 if(range_header) { 585 int status; 586 range = parse_range(sn, range_header, &status); 587 if(status != PROTOCOL_OK) { 588 protocol_status(sn, rq, status, NULL); 589 vfs_close(fd); 590 return REQ_ABORTED; 591 } 592 593 if(!validate_range(range, &s, &status)) { 594 protocol_status(sn, rq, status, NULL); 595 free_range(sn, range); 596 vfs_close(fd); 597 return REQ_ABORTED; 598 } 599 } 600 601 int single_range = 1; 602 off_t offset; 603 off_t length; 604 if(range) { 605 protocol_status(sn, rq, 206, NULL); 606 pblock_removekey(pb_key_content_length, rq->srvhdrs); 607 608 if(range->next) { 609 single_range = 0; 610 } else { 611 range2off(range, s.st_size, &offset, &length); 612 613 pblock_kllinsert( 614 pb_key_content_length, 615 (long long)length, 616 rq->srvhdrs); 617 618 sstr_t content_range = ucx_sprintf( 619 "%lld-%lld/%lld", 620 (long long)offset, 621 (long long)offset+length - 1, 622 s.st_size); 623 pblock_kvinsert( 624 pb_key_content_range, 625 content_range.ptr, 626 content_range.length, 627 rq->srvhdrs); 628 free(content_range.ptr); 629 } 630 } else { 631 offset = 0; 632 length = s.st_size; 633 } 634 635 int ret = REQ_NOACTION; 636 if(single_range) { 637 // send response header 638 http_start_response(sn, rq); 639 // send content 640 // TODO: fix: send_range_aio is unstable 641 //ret = send_range_aio(sn, rq, fd, offset, length, NULL, 0); 642 //if(ret == REQ_PROCESSING) { 643 // return ret; 644 //} 645 646 if(send_range(sn, fd, offset, length, NULL, 0)) { 647 // TODO: error 648 } 649 } else { 650 ret = send_multi_range(sn, rq, fd, s.st_size, range); 651 // TODO: error 652 } 653 654 // cleanup 655 vfs_close(fd); 656 free_range(sn, range); 657 658 return ret; 659 } 660 661 662 663 int service_hello(pblock *pb, Session *sn, Request *rq) { 664 pblock_removekey(pb_key_content_type, rq->srvhdrs); 665 pblock_nvinsert("content-type", "text/plain", rq->srvhdrs); 666 pblock_nninsert("content-length", 13, rq->srvhdrs); 667 protocol_status(sn, rq, 200, NULL); 668 http_start_response(sn, rq); 669 net_write(sn->csd, "Hello World!\n", 13); 670 return REQ_PROCEED; 671 } 672 673 static int ws_msghandler(WebSocket *ws, WSMessage *msg) { 674 if(msg->type == 1) { 675 printf("Message(text): %.*s\n", (int)msg->length, msg->data); 676 websocket_send_text(ws->userdata, "hello", 5); 677 } else { 678 printf("Message: opcode: %d | length: %d\n", msg->type, (int)msg->length); 679 } 680 return 0; 681 } 682 683 int service_ws_hello(pblock *pb, Session *sn, Request *rq) { 684 WebSocket ws; 685 ZERO(&ws, sizeof(WebSocket)); 686 ws.userdata = sn->csd; 687 688 ws.on_message = ws_msghandler; 689 return http_handle_websocket(sn, rq, &ws); 690 } 691 692 int service_index(pblock *pb, Session *sn, Request *rq) { 693 //printf("service_index\n"); 694 695 char *path = pblock_findkeyval(pb_key_path, rq->vars); 696 char *uri = pblock_findkeyval(pb_key_uri, rq->reqpb); 697 698 sstr_t r_uri = sstr(uri); 699 700 // open the file 701 VFSContext *vfs = vfs_request_context(sn, rq); 702 VFS_DIR dir = vfs_opendir(vfs, path); 703 if(!dir) { 704 return REQ_ABORTED; 705 } 706 707 sbuf_t *out = sbuf_new(1024); // output buffer 708 709 // write html header 710 sbuf_puts(out, "<html>\n<head>\n<title>Index of "); 711 sbuf_puts(out, uri); 712 sbuf_puts(out, "</title>\n</head><body>\n<h1>Index of "); 713 sbuf_puts(out, uri); 714 sbuf_puts(out, "</h1><hr>\n\n"); 715 716 // list directory 717 VFS_ENTRY f; 718 while(vfs_readdir(dir, &f)) { 719 sstr_t filename = sstr(f.name); 720 721 sbuf_puts(out, "<a href=\""); 722 sbuf_append(out, r_uri); 723 sbuf_append(out, filename); 724 sbuf_puts(out, "\">"); 725 sbuf_append(out, filename); 726 sbuf_puts(out, "</a><br>\n"); 727 } 728 729 sbuf_puts(out, "\n</body>\n</html>\n"); 730 731 // send stuff to client 732 pblock_removekey(pb_key_content_type, rq->srvhdrs); 733 pblock_kvinsert(pb_key_content_type, "text/html", 9, rq->srvhdrs); 734 pblock_nninsert("content-length", out->length, rq->srvhdrs); 735 protocol_status(sn, rq, 200, NULL); 736 http_start_response(sn, rq); 737 738 net_write(sn->csd, out->ptr, out->length); 739 740 // close 741 vfs_closedir(dir); 742 sbuf_free(out); 743 744 return REQ_PROCEED; 745 } 746 747 int send_options(pblock *pb, Session *sn, Request *rq) { 748 char *allow = "HEAD, GET, PUT, DELETE, TRACE, OPTIONS, MOVE, COPY, " 749 "PROPFIND, PROPPATCH, MKCOL, LOCK, UNLOCK, ACL, REPORT"; 750 char *dav = "1,2,access-control"; 751 752 pblock_removekey(pb_key_content_type, rq->srvhdrs); 753 pblock_nvinsert("allow", allow, rq->srvhdrs); 754 pblock_nvinsert("dav", dav, rq->srvhdrs); 755 pblock_nninsert("content-length", 0, rq->srvhdrs); 756 protocol_status(sn, rq, 204, NULL); 757 http_start_response(sn, rq); 758 759 return REQ_PROCEED; 760 } 761