/* * Copyright (C) 2012-2020 all contributors * License: GPL-3.0+ */ #include "cmogstored.h" #include "http.h" #if defined(HAVE_SYS_SENDFILE_H) && !defined(HAVE_BSD_SENDFILE) # include #endif #if defined(__linux__) /* all good */ #elif defined(HAVE_SENDFILE) || defined(HAVE_BSD_SENDFILE) # if defined(HAVE_BSD_SENDFILE) && !defined(HAVE_SENDFILE)/* Debian kFBSD */ # define sendfile(fd,s,offset,nbytes,hdtr,sbytes,flags) \ bsd_sendfile((fd),(s),(offset),(nbytes),(hdtr),(sbytes),(flags)) # endif /* HAVE_BSD_SENDFILE */ /* * make BSD sendfile look like Linux for now... * we can support SF_NODISKIO later */ static ssize_t linux_sendfile(int sockfd, int filefd, off_t *off, size_t count) { int flags = 0; off_t sbytes = 0; int rc; rc = sendfile(filefd, sockfd, *off, count, NULL, &sbytes, flags); if (sbytes > 0) { *off += sbytes; return (ssize_t)sbytes; } return (ssize_t)rc; } # if defined(HAVE_BSD_SENDFILE) /* Debian GNU/kFreeBSD */ # undef sendfile # endif /* HAVE_BSD_SENDFILE */ # define sendfile(out_fd, in_fd, offset, count) \ linux_sendfile((out_fd),(in_fd),(offset),(count)) #else # include "compat_sendfile.h" #endif #define ERR416 "416 Requested Range Not Satisfiable" static void http_hdr_prepare(char **buf, char **modified, size_t *len, time_t *mtime) { /* single buffer so we can use MSG_MORE */ *buf = mog_fsbuf_get(len); *modified = *buf + *len / 2; assert((*len / 2) > MOG_HTTPDATE_CAPA && "fsbuf too small"); mog_http_date(*modified, MOG_HTTPDATE_CAPA, mtime); } /* * TODO: refactor this * * snprintf() usage here is a hot spot in profiling. Perhaps one day, * link-time optimization will be able to work on *printf() functions * so we won't hurt code maintainability by optimizing away snprintf() * ourselves. This function is ugly enough already */ static off_t http_get_resp_hdr(struct mog_fd *mfd, struct stat *sb) { struct mog_http *http = &mfd->as.http; char *buf, *modified; size_t len; struct mog_now *now = mog_now(); long long count; int rc; http_hdr_prepare(&buf, &modified, &len, &sb->st_mtime); /* validate ranges */ if (http->_p.has_range) { long long offset; if (http->_p.range_end < 0 && http->_p.range_beg < 0) goto bad_range; if (http->_p.range_beg >= sb->st_size) goto bad_range; /* bytes=M-N where M > N */ if (http->_p.range_beg >= 0 && http->_p.range_end >= 0 && http->_p.range_beg > http->_p.range_end) goto bad_range; if (http->_p.range_end < 0) { /* bytes=M- */ /* bytes starting at M until EOF */ assert(http->_p.range_beg >= 0 && "should've sent 416"); offset = (long long)http->_p.range_beg; count = (long long)(sb->st_size - offset); } else if (http->_p.range_beg < 0) { /* bytes=-N */ /* last N bytes */ assert(http->_p.range_end >= 0 && "should've sent 416"); offset = (long long)(sb->st_size - http->_p.range_end); /* serve the entire file if client requested too much */ if (offset < 0) goto resp_200; count = (long long)(sb->st_size - offset); } else { /* bytes=M-N*/ assert(http->_p.range_beg >= 0 && http->_p.range_end >= 0 && "should've sent 416"); offset = (long long)http->_p.range_beg; /* truncate responses to current file size */ if (http->_p.range_end >= sb->st_size) http->_p.range_end = sb->st_size - 1; count = (long long)http->_p.range_end + 1 - offset; } assert(count > 0 && "bad count for 206 response"); assert(offset >= 0 && "bad offset for 206 response"); if (http->forward) { struct mog_file *file = &http->forward->as.file; file->foff = offset; file->fsize = (off_t)(offset + count); } rc = snprintf(buf, len, "HTTP/1.1 206 Partial Content\r\n" "Date: %s\r\n" "Last-Modified: %s\r\n" "Content-Length: %lld\r\n" "Content-Type: application/octet-stream\r\n" "Content-Range: bytes %lld-%lld/%lld\r\n" "Connection: %s\r\n" "\r\n", now->httpdate, modified, count, /* Content-Length */ offset, offset + count - 1, /* bytes M-N */ (long long)sb->st_size, http->_p.persistent ? "keep-alive" : "close"); } else if (http->_p.bad_range) { goto bad_range; } else { resp_200: count = (long long)sb->st_size; rc = snprintf(buf, len, "HTTP/1.1 200 OK\r\n" "Date: %s\r\n" "Last-Modified: %s\r\n" "Content-Length: %lld\r\n" "Content-Type: application/octet-stream\r\n" "Accept-Ranges: bytes\r\n" "Connection: %s\r\n" "\r\n", now->httpdate, modified, count, http->_p.persistent ? "keep-alive" : "close"); } /* TODO: put down the crack pipe and refactor this */ if (0) { bad_range: count = 0; if (http->forward) { mog_file_close(http->forward); http->forward = NULL; } else { assert(http->_p.http_method == MOG_HTTP_METHOD_HEAD && "not HTTP HEAD"); } rc = snprintf(buf, len, "HTTP/1.1 " ERR416 "\r\n" "Date: %s\r\n" "Content-Length: 0\r\n" "Content-Type: text/plain\r\n" "Content-Range: bytes */%lld\r\n" "Connection: %s\r\n" "\r\n", now->httpdate, (long long)sb->st_size, http->_p.persistent ? "keep-alive" : "close"); } assert(rc > 0 && rc < len && "we suck at snprintf"); len = (size_t)rc; assert(http->wbuf == NULL && "tried to write to a busy client"); if (http->_p.http_method == MOG_HTTP_METHOD_HEAD) count = 0; TRACE(CMOGSTORED_HTTP_RES_START(mfd->fd, buf + sizeof("HTTP/1.1"))); http->wbuf = mog_trysend(mfd->fd, buf, len, (off_t)count); return (off_t)count; } static void emit_dev_usage(struct mog_fd *mfd) { struct mog_http *http = &mfd->as.http; struct mog_dev *dev = mog_dev_for(http->svc, http->_p.mog_devid, false); void *ok = NULL; if (dev) { char *buf, *modified; size_t len, ilen; struct mog_now *now; int rc; bool retried = false; struct iovec iov; retry: now = mog_now(); CHECK(int, 0, pthread_mutex_lock(&dev->usage_lock)); ok = dev->usage_txt; if (!ok) { if (retried) goto out_unlock; retried = true; CHECK(int, 0, pthread_mutex_unlock(&dev->usage_lock)); mog_dev_usage_update(dev, http->svc); goto retry; } http_hdr_prepare(&buf, &modified, &len, &dev->usage_mtime); ilen = len; rc = snprintf(buf, len, "HTTP/1.1 200 OK\r\n" "Date: %s\r\n" "Last-Modified: %s\r\n" "Content-Length: %u\r\n" "Content-Type: text/plain\r\n" "Accept-Ranges: bytes\r\n" "Connection: %s\r\n" "\r\n", now->httpdate, modified, dev->usage_len, http->_p.persistent ? "keep-alive" : "close"); ok = NULL; if (rc > 0) { len -= rc; if (http->_p.http_method == MOG_HTTP_METHOD_HEAD) { ok = iov.iov_base = buf; iov.iov_len = rc; } else if (len >= dev->usage_len && len < ilen) { memcpy(buf + rc, dev->usage_txt, dev->usage_len); ok = iov.iov_base = buf; iov.iov_len = rc + dev->usage_len; } } out_unlock: CHECK(int, 0, pthread_mutex_unlock(&dev->usage_lock)); if (ok) http->wbuf = mog_trywritev(mfd->fd, &iov, 1); } if (!dev || !ok) mog_http_resp(mfd, "404 Not Found", true); } void mog_http_get_open(struct mog_fd *mfd, char *buf) { struct mog_http *http = &mfd->as.http; struct stat sb; struct mog_file *file = NULL; char *path; off_t len; if (http->_p.usage_txt) { emit_dev_usage(mfd); return; } path = mog_http_path(http, buf); if (!path) goto forbidden; /* path traversal attack */ assert(http->forward == NULL && "already have http->forward"); assert(path[0] == '/' && "bad path"); TRACE(CMOGSTORED_HTTP_REQ_START(mfd->fd, http->_p.http_method == MOG_HTTP_METHOD_HEAD ? "HEAD" : "GET", path)); if (path[1] == '\0') { /* keep "mogadm check" happy */ sb.st_mtime = 0; sb.st_size = 0; } else if (http->_p.http_method == MOG_HTTP_METHOD_HEAD) { if (mog_stat(http->svc, path, &sb) < 0) goto err; if (!S_ISREG(sb.st_mode)) goto forbidden; } else { http->forward = mog_file_open_read(http->svc, path); if (http->forward == NULL) goto err; file = &http->forward->as.file; assert(file->path == NULL && "build system bug"); if (fstat(http->forward->fd, &sb) < 0) { PRESERVE_ERRNO( mog_file_close(http->forward) ); http->forward = NULL; goto err; } if (!S_ISREG(sb.st_mode)) { mog_file_close(http->forward); http->forward = NULL; goto forbidden; } file->fsize = sb.st_size; } len = http_get_resp_hdr(mfd, &sb); /* http->forward may be NULL even if file is set if we had an error */ if (http->wbuf == NULL && http->forward) { assert(file && "file unset but http->forward is set"); if (len > (256 * 1024)) mog_fadv_sequential(http->forward->fd, file->foff, len); } return; err: switch (errno) { case EACCES: forbidden: mog_http_resp(mfd, "403 Forbidden", true); return; case ENOENT: mog_http_resp(mfd, "404 Not Found", true); return; } PRESERVE_ERRNO(do { mog_http_resp(mfd, "500 Internal Server Error", true); } while(0)); } static void sendfile_error(struct mog_fd *file_mfd, int sferr) { struct mog_file *file = &file_mfd->as.file; struct stat st; if (!fstat(file_mfd->fd, &st)) { errno = sferr; syslog(LOG_ERR, "sendfile on (dev=%lu,ino=%lu) failed at offset=%lld: %m", (unsigned long)st.st_dev, (unsigned long)st.st_ino, (long long)file->foff); } else { syslog(LOG_ERR, "sendfile failed at offset=%lld: %s (fstat error: %m)", (long long)file->foff, strerror(sferr)); } } enum mog_next mog_http_get_in_progress(struct mog_fd *mfd) { struct mog_http *http = &mfd->as.http; struct mog_fd *file_mfd; struct mog_file *file; ssize_t w; off_t count; off_t max_sendfile = (mog_ioq_contended() ? 1 : 100) * 1024 * 1024; assert(http->wbuf == NULL && "can't serve file with http->wbuf"); assert(http->forward && http->forward != MOG_IOSTAT && "bad forward"); file_mfd = http->forward; file = &file_mfd->as.file; assert(file->fsize >= 0 && "fsize is negative"); assert(file->foff >= 0 && "foff is negative"); count = file->fsize - file->foff; count = count > max_sendfile ? max_sendfile : count; if (count == 0) goto done; retry: w = sendfile(mfd->fd, file_mfd->fd, &file->foff, (size_t)count); if (w > 0) { if (file->foff == file->fsize) goto done; return MOG_NEXT_ACTIVE; } else if (w < 0) { switch (errno) { case_EAGAIN: return MOG_NEXT_WAIT_WR; case EINTR: goto retry; case EPIPE: case ENOTCONN: case ECONNRESET: break; default: sendfile_error(file_mfd, errno); } http->_p.persistent = 0; } else { /* w == 0 */ /* * if we can't fulfill the value set by our Content-Length: * header, we must kill the TCP connection */ http->_p.persistent = 0; syslog(LOG_ERR, "sendfile()-d 0 bytes at offset=%lld; file truncated?", (long long)file->foff); } done: TRACE(CMOGSTORED_HTTP_BYTES_XFER(mfd->fd, file->foff)); mog_file_close(http->forward); if (http->_p.persistent) { mog_http_reset(mfd); return MOG_NEXT_ACTIVE; } return MOG_NEXT_CLOSE; }