Sun, 11 Jun 2023 15:53:55 +0200
fix non-blocking CGI handler and non-blocking SSL-IO
/* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright 2013 Olaf Wintermann. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ #include <stdio.h> #include <sys/file.h> #include <sys/stat.h> #include "service.h" #include "objecttype.h" #include "../util/io.h" #include "../util/pblock.h" #include "../util/util.h" #include "../daemon/protocol.h" #include "../daemon/vfs.h" #include "../util/strbuf.h" #include <cx/string.h> #include <cx/utils.h> #include <cx/printf.h> #include <errno.h> /* * prepares servicing a file * * adds content-length header * * return the opened file */ SYS_FILE prepare_service_file(Session *sn, Request *rq, VFSContext *vfs, struct stat *s, int *ret) { char *path = pblock_findkeyval(pb_key_path, rq->vars); // open the file SYS_FILE fd = vfs_open(vfs, path, O_RDONLY); if(!fd) { // vfs_open sets http status code *ret = REQ_ABORTED; return NULL; } // get stat if(vfs_fstat(vfs, fd, s) != 0) { //perror("prepare_service_file: stat"); protocol_status(sn, rq, 500, NULL); *ret = REQ_ABORTED; return NULL; } // check if the file is a directory if(S_ISDIR(s->st_mode)) { pblock_removekey(pb_key_content_type, rq->srvhdrs); char *uri = pblock_findkeyval(pb_key_uri, rq->reqpb); size_t urilen = strlen(uri); if(urilen > 0 && uri[urilen-1] != '/') { pblock_nvinsert("content-length", "0", rq->srvhdrs); char *location = pool_malloc(sn->pool, urilen + 2); memcpy(location, uri, urilen); location[urilen] = '/'; location[urilen+1] = '\0'; pblock_kvinsert(pb_key_location, location, urilen + 1, rq->srvhdrs); protocol_status(sn, rq, 302, NULL); http_start_response(sn, rq); *ret = REQ_PROCEED; } else { // set content-type to "internal/directory" and ret to REQ_NOACTION // maybe a SAF will respond to that pblock_kvinsert( pb_key_content_type, OBJTYPE_INTERNAL_DIRECTORY, sizeof(OBJTYPE_INTERNAL_DIRECTORY)-1, rq->srvhdrs); *ret = REQ_NOACTION; } vfs_close(fd); return NULL; } // sets last-modified, content-length and checks conditions const char *etag = vfs_getetag(fd); // optionally, get etag from file if(http_set_finfo_etag(sn, rq, s, etag) != REQ_PROCEED) { vfs_close(fd); *ret = REQ_ABORTED; return NULL; } // TODO: check if vfs can seek pblock_kvinsert(pb_key_accept_ranges, "bytes", 5, rq->srvhdrs); // start response protocol_status(sn, rq, 200, NULL); return fd; } static void free_range(Session *sn, HttpRange *range) { HttpRange *elm = range; while(elm) { HttpRange *next = elm->next; pool_free(sn->pool, elm); elm = next; } } static HttpRange* parse_range(Session *sn, char *header, int *status) { *status = PROTOCOL_OK; cxstring range = cx_strtrim(cx_str(header)); if(!cx_strprefix(range, (cxstring)CX_STR("bytes="))) { // unknown range unit - ignore range header return NULL; } // get byte-range-set range = cx_strsubs(range, 6); if(range.length < 1) { return NULL; } HttpRange *range_list = NULL; HttpRange *last = NULL; off_t begin = -1; int start = 0; int hasbegin = 0; for(int i=0;i<=range.length;i++) { char c = range.ptr[i]; if(c == '-') { cxstring num = cx_strsubsl(range, start, i-start); if(num.length == 0) { // empty string before '-' is legal hasbegin = 1; begin = -1; start = i+1; continue; } char *end; long long n = strtoll(num.ptr, &end, 10); if(errno == 0 && end == range.ptr + i && n >= 0) { begin = n; hasbegin = 1; start = i+1; } else { // syntax error free_range(sn, range_list); return NULL; } } else if(c == ',' || c == '\0') { cxstring num = cx_strsubsl(range, start, i-start); if(hasbegin) { long long n; if(num.length == 0) { // empty string after '-' is legal n = -1; } else { char *end; n = strtoll(num.ptr, &end, 10); if(errno != 0 || end != range.ptr + i || n < 0) { // syntax error free_range(sn, range_list); return NULL; } } if(!(begin < 0 && n < 0)) { // range: begin - n HttpRange *rangeelm = pool_malloc(sn->pool, sizeof(HttpRange)); if(!rangeelm) { free_range(sn, range_list); *status = PROTOCOL_SERVER_ERROR; return NULL; } rangeelm->begin = begin; rangeelm->end = n; rangeelm->next = NULL; if(!last) { range_list = rangeelm; last = rangeelm; } else { last->next = rangeelm; last = rangeelm; } hasbegin = 0; start = i+1; continue; } } // syntax error free_range(sn, range_list); return NULL; } } return range_list; } static int validate_range(HttpRange *range, struct stat *finfo, int *status) { off_t max_len = finfo->st_size; while(range) { if(range->begin > 0 && range->end > 0) { if(range->end < range->begin) { *status = PROTOCOL_REQUESTED_RANGE_NOT_SATISFIABLE; return 0; } } if(range->begin >= max_len) { *status = PROTOCOL_REQUESTED_RANGE_NOT_SATISFIABLE; return 0; } if(range->end >= max_len) { *status = PROTOCOL_REQUESTED_RANGE_NOT_SATISFIABLE; return 0; } range = range->next; } // TODO: check for Denial-of-Service Attacks return 1; } /* * translates a HttpRange element to a begin offset and a length * the HttpRange must be validated */ static void range2off(HttpRange *range, off_t filelen, off_t *begin, off_t *length) { if(range->begin < 0) { // bytes=-a *begin = filelen - range->end; *length = range->end; } else if(range->end < 0) { // bytes=a- *begin = range->begin; *length = filelen - range->begin; } else { // bytes=a-b *begin = range->begin; *length = range->end + 1 - range->begin; } } #define SF_MAX_LEN 0x8000000 static int send_range(Session *sn, SYS_FILE fd, off_t offset, off_t length, char *header, int headerlen) { off_t remaining = length; sendfiledata sfd; sfd.fd = fd; sfd.header = header; sfd.hlen = headerlen; sfd.trailer = NULL; sfd.tlen = 0; while(remaining > 0) { size_t sflen = remaining < SF_MAX_LEN ? remaining : SF_MAX_LEN; sfd.offset = offset; sfd.len = sflen; ssize_t r = net_sendfile(sn->csd, &sfd); if(r < 0) { return -1; } sfd.header = NULL; // make sure the header is only sent once offset += r; remaining -= r; } return 0; } static void send_range_cleanup(AsyncSendRange *asr) { WSBool error = asr->error; Session *sn = asr->sn; Request *rq = asr->rq; pool_handle_t *pool = asr->sn->pool; vfs_close(asr->in); pool_free(pool, asr->aio->buf); pool_free(pool, asr->aio); pool_free(pool, asr->readev); pool_free(pool, asr->writeev); pool_free(pool, asr); int ret = REQ_PROCEED; if(error) { rq->rq_attr.keep_alive = 0; ret = REQ_ABORTED; } // return to nsapi loop nsapi_function_return(sn, rq, ret); } static int send_buf( SYS_NETFD out, char *restrict buf, size_t len, size_t *restrict pos) { while(*pos < len) { ssize_t w = net_write(out, buf + *pos, len - *pos); if(w <= 0) { return -1; } *pos += w; } return 0; } static int send_bytes(AsyncSendRange *asr, WSBool *completed) { *completed = FALSE; if(asr->header) { if(send_buf(asr->out, asr->header, asr->headerlen, &asr->headerpos)) { if(net_errno(asr->out) == EAGAIN) { return 0; } else { asr->error = TRUE; return 1; } } if(asr->headerpos >= asr->headerlen) { asr->header = NULL; } } if(send_buf(asr->out, asr->aio->buf, asr->aio->result, &asr->wpos)) { if(net_errno(asr->out) == EAGAIN) { return 0; } else { asr->error = TRUE; return 1; } } if(!asr->read_complete) { // write completed => new asynchronous read asr->aio->offset += asr->aio->result; size_t length = asr->end - asr->offset; asr->aio->nbytes = AIO_BUF_SIZE < length ? AIO_BUF_SIZE : length; asr->read_inprogress = TRUE; if(system_aio_read(asr->aio)) { asr->error = TRUE; return 1; } } *completed = TRUE; return 0; } static int send_range_readevent(EventHandler *ev, Event *event) { AsyncSendRange *asr = event->cookie; asr->read_inprogress = FALSE; asr->wpos = 0; asr->offset += asr->aio->result; if(asr->error || asr->aio->result < 0) { return 0; } int ret = 1; if(asr->aio->result == 0 || asr->offset >= asr->end) { asr->read_complete = TRUE; ret = 0; } WSBool completed; if(send_bytes(asr, &completed)) { return 0; } if(!completed && !asr->write_inprogress) { asr->write_inprogress = TRUE; if(event_pollout(ev, asr->out, asr->writeev)) { asr->error = TRUE; return 0; } } return ret; } static int send_range_writeevent(EventHandler *ev, Event *event) { AsyncSendRange *asr = event->cookie; if(asr->error) { return 1; } WSBool completed; if(send_bytes(asr, &completed)) { return 1; } if(completed) { return 0; } return 1; } static int send_range_aio_finish(EventHandler *ev, Event *event) { AsyncSendRange *asr = event->cookie; if(!asr->write_inprogress) { send_range_cleanup(asr); } asr->read_inprogress = FALSE; return 0; } static int send_range_poll_finish(EventHandler *ev, Event *event) { AsyncSendRange *asr = event->cookie; if(!asr->read_inprogress) { send_range_cleanup(asr); } asr->write_inprogress = FALSE; return 0; } static int send_range_aio(Session *sn, Request *rq, SYS_FILE fd, off_t offset, off_t length, char *header, int headerlen) { net_setnonblock(sn->csd, TRUE); // try to send the header ssize_t hw = net_write(sn->csd, header, headerlen); if(hw < 0) { if(net_errno(sn->csd) == EAGAIN) { hw = 0; } else { return REQ_ABORTED; } } AsyncSendRange *asr = pool_malloc(sn->pool, sizeof(AsyncSendRange)); asr->sn = sn; asr->rq = rq; asr->in = fd; asr->out = sn->csd; asr->offset = offset; asr->end = offset + length; //asr->length = length; asr->pos = offset; asr->read_complete = FALSE; asr->read_inprogress = FALSE; asr->write_inprogress = FALSE; asr->error = FALSE; if(hw == headerlen) { asr->header = NULL; asr->headerlen = 0; asr->headerpos = 0; } else { asr->header = header; asr->headerlen = headerlen; asr->headerpos = hw; } Event *readev = pool_malloc(sn->pool, sizeof(Event)); ZERO(readev, sizeof(Event)); readev->cookie = asr; readev->fn = send_range_readevent; readev->finish = send_range_aio_finish; Event *writeev = pool_malloc(sn->pool, sizeof(Event)); ZERO(writeev, sizeof(Event)); writeev->cookie = asr; writeev->fn = send_range_writeevent; writeev->finish = send_range_poll_finish; asr->readev = readev; asr->writeev = writeev; aiocb_s *aio = pool_malloc(sn->pool, sizeof(aiocb_s)); aio->buf = pool_malloc(sn->pool, AIO_BUF_SIZE); aio->nbytes = AIO_BUF_SIZE < length ? AIO_BUF_SIZE : length; aio->filedes = fd; aio->offset = offset; aio->evhandler = sn->ev; aio->event = readev; asr->aio = aio; asr->wpos = 0; asr->read_inprogress = TRUE; if(system_aio_read(aio)) { send_range_cleanup(asr); return REQ_ABORTED; } asr->read_inprogress = TRUE; return REQ_PROCESSING; } struct multi_range_elm { cxmutstr header; off_t offset; off_t length; }; static int send_multi_range(Session *sn, Request *rq, SYS_FILE fd, off_t filelen, HttpRange *range) { CxAllocator *a = pool_allocator(sn->pool); pb_param *content_type = pblock_remove("content-type", rq->srvhdrs); char sep[64]; int seplen = util_mime_separator(sep); cxmutstr newct = cx_asprintf_a(a, "multipart/byteranges; boundary=%s", sep+4); pblock_kvinsert( pb_key_content_type, newct.ptr, newct.length, rq->srvhdrs); cxFree(a, newct.ptr); // calculate content-length off_t response_len = 0; int nrange = 0; HttpRange *rangeelm = range; while(rangeelm) { nrange++; rangeelm = rangeelm->next; } struct multi_range_elm *r = pool_calloc(sn->pool, nrange, sizeof(struct multi_range_elm)); rangeelm = range; int i=0; while(rangeelm) { range2off(rangeelm, filelen, &(r[i].offset), &(r[i].length)); r[i].header = cx_asprintf_a( a, "%s\r\nContent-Type: %s\r\nContent-Range: bytes %lld-%lld/%lld\r\n\r\n", sep, content_type->value, (long long)r[i].offset, (long long)r[i].offset+r[i].length - 1, (long long)filelen); response_len += r[i].header.length + r[i].length; rangeelm = rangeelm->next; i++; } response_len += seplen + 4; // trailer: sep + '--' + CRLF // finally, set the content-length header pblock_kllinsert( pb_key_content_length, (long long)response_len, rq->srvhdrs); // and start the response http_start_response(sn, rq); rangeelm = range; i = 0; while(rangeelm) { if(send_range(sn, fd, r[i].offset, r[i].length, r[i].header.ptr, r[i].header.length)) { // TODO: error } rangeelm = rangeelm->next; i++; } net_printf(sn->csd, "%s--\r\n", sep); pool_free(sn->pool, r); return 0; } int send_file(pblock *pb, Session *sn, Request *rq) { int ret = REQ_NOACTION; struct stat s; VFSContext *vfs = vfs_request_context(sn, rq); SYS_FILE fd = prepare_service_file(sn, rq, vfs, &s, &ret); if(!fd) { // if an error occurs, prepare_service_file sets the http status code // in case fd is a directory and the uri already ends with an trailing // '/', ret is set to REQ_NOACTION return ret; } // get and validate range header char *range_header = pblock_findkeyval(pb_key_range, rq->headers); HttpRange *range = NULL; if(range_header) { log_ereport(LOG_DEBUG, "send_file: range: %s", range_header); int status; range = parse_range(sn, range_header, &status); if(status != PROTOCOL_OK) { protocol_status(sn, rq, status, NULL); vfs_close(fd); return REQ_ABORTED; } if(!validate_range(range, &s, &status)) { protocol_status(sn, rq, status, NULL); free_range(sn, range); vfs_close(fd); return REQ_ABORTED; } } int single_range = 1; off_t offset; off_t length; if(range) { protocol_status(sn, rq, 206, NULL); pblock_removekey(pb_key_content_length, rq->srvhdrs); if(range->next) { single_range = 0; } else { range2off(range, s.st_size, &offset, &length); pblock_kllinsert( pb_key_content_length, (long long)length, rq->srvhdrs); cxmutstr content_range = cx_asprintf( "%lld-%lld/%lld", (long long)offset, (long long)offset+length - 1, (long long)s.st_size); pblock_kvinsert( pb_key_content_range, content_range.ptr, content_range.length, rq->srvhdrs); free(content_range.ptr); } } else { offset = 0; length = s.st_size; } if(single_range) { // send response header http_start_response(sn, rq); // send content // TODO: fix: send_range_aio is unstable #96 //ret = send_range_aio(sn, rq, fd, offset, length, NULL, 0); //if(ret == REQ_PROCESSING) { // return ret; //} if(send_range(sn, fd, offset, length, NULL, 0)) { // TODO: error } } else { ret = send_multi_range(sn, rq, fd, s.st_size, range); // TODO: error } // cleanup vfs_close(fd); free_range(sn, range); return ret; } int service_hello(pblock *pb, Session *sn, Request *rq) { pblock_removekey(pb_key_content_type, rq->srvhdrs); pblock_nvinsert("content-type", "text/plain", rq->srvhdrs); pblock_nninsert("content-length", 13, rq->srvhdrs); protocol_status(sn, rq, 200, NULL); http_start_response(sn, rq); net_write(sn->csd, "Hello World!\n", 13); return REQ_PROCEED; } static int ws_msghandler(WebSocket *ws, WSMessage *msg) { if(msg->type == 1) { printf("Message(text): %.*s\n", (int)msg->length, msg->data); websocket_send_text(ws->userdata, "hello", 5); } else { printf("Message: opcode: %d | length: %d\n", msg->type, (int)msg->length); } return 0; } int service_ws_hello(pblock *pb, Session *sn, Request *rq) { WebSocket ws; ZERO(&ws, sizeof(WebSocket)); ws.userdata = sn->csd; ws.on_message = ws_msghandler; return http_handle_websocket(sn, rq, &ws); } int service_index(pblock *pb, Session *sn, Request *rq) { //printf("service_index\n"); char *path = pblock_findkeyval(pb_key_path, rq->vars); char *uri = pblock_findkeyval(pb_key_uri, rq->reqpb); cxstring r_uri = cx_str(uri); // open the file VFSContext *vfs = vfs_request_context(sn, rq); VFS_DIR dir = vfs_opendir(vfs, path); if(!dir) { return REQ_ABORTED; } sbuf_t *out = sbuf_new(1024); // output buffer // write html header sbuf_puts(out, "<html>\n<head>\n<title>Index of "); sbuf_puts(out, uri); sbuf_puts(out, "</title>\n</head><body>\n<h1>Index of "); sbuf_puts(out, uri); sbuf_puts(out, "</h1><hr>\n\n"); // list directory VFS_ENTRY f; while(vfs_readdir(dir, &f)) { cxstring filename = cx_str(f.name); sbuf_puts(out, "<a href=\""); sbuf_append(out, r_uri); sbuf_append(out, filename); sbuf_puts(out, "\">"); sbuf_append(out, filename); sbuf_puts(out, "</a><br>\n"); } sbuf_puts(out, "\n</body>\n</html>\n"); // send stuff to client pblock_removekey(pb_key_content_type, rq->srvhdrs); pblock_kvinsert(pb_key_content_type, "text/html", 9, rq->srvhdrs); pblock_nninsert("content-length", out->length, rq->srvhdrs); protocol_status(sn, rq, 200, NULL); http_start_response(sn, rq); net_write(sn->csd, out->ptr, out->length); // close vfs_closedir(dir); sbuf_free(out); return REQ_PROCEED; } int send_options(pblock *pb, Session *sn, Request *rq) { char *allow = "HEAD, GET, PUT, DELETE, TRACE, OPTIONS, MOVE, COPY, " "PROPFIND, PROPPATCH, MKCOL, LOCK, UNLOCK, ACL, REPORT"; char *dav = "1,2,access-control"; pblock_removekey(pb_key_content_type, rq->srvhdrs); pblock_nvinsert("allow", allow, rq->srvhdrs); pblock_nvinsert("dav", dav, rq->srvhdrs); pblock_nninsert("content-length", 0, rq->srvhdrs); protocol_status(sn, rq, 204, NULL); http_start_response(sn, rq); return REQ_PROCEED; }