/* * Copyright (c) 2021 Maurizio Lombardi * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * - Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * - 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. * - The name of the author may not be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``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 AUTHOR 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. */ #ifndef _FREEBSD_ #define _GNU_SOURCE #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifndef _FREEBSD_ #define MAP_PREFAULT_READ MAP_POPULATE #endif #define SRV_PORT 47000 #define BUFSIZE 4096 #define BASE_PATH "/home/mlombardi/public_html" #define WEBSITE_ADDR "http://bsdbackstore.eu" static int base_path_len; enum { REQ_TYPE_GET, REQ_TYPE_HEAD, REQ_TYPE_POST, REQ_TYPE_UNSUP, }; struct th_data { int conn_fd; unsigned thread_id; }; struct content_range { size_t start; size_t end; }; static struct { char *ext; char *ctype; } fname_type_table[] = { ".html", "text/html", ".ico", "image/x-icon", ".txt", "text/plain", ".c", "text/plain", ".patch", "text/plain", ".log", "text/plain", ".jpg", "image/jpeg", ".jpeg", "image/jpeg", ".png", "image/png", ".gif", "image/gif", ".bz2", "application/x-bzip2", ".gz", "application/gzip", ".pdf", "application/pdf", ".tar", "application/x-tar", ".zip", "application/zip", ".sh", "text/plain", ".rs", "text/plain", ".cpp", "text/plain", ".diff", "text/plain", ".mp4", "video/mp4", ".mp3", "audio/mp3", ".aac", "audio/aac", ".conf", "text/plain", ".py", "text/plain", NULL, NULL }; static void* handle_connection(void *data); static int req_type(char const *req); static int handle_request(char *req, int len, int conn_fd, unsigned th_id); static int handle_request_get(char *fname, int conn_fd, unsigned th_id, char *content); static int handle_request_head(int conn_fd); static int send_dir_to_client(char *dir_path, int conn_fd); static int send_file_to_client(int file_fd, int conn_fd, size_t file_size, char *file_type, struct content_range *range); static char *filename_to_contenttype(char *fname); static int send_moved_document_page(char *dest, int conn_fd); static struct content_range * get_content_range(char *req); int main(int argc, char **argv) { int s, r, thread_id = 0; struct sockaddr_in srv_addr; struct sockaddr_in client_addr; pthread_attr_t th_attr; sigaction(SIGPIPE, &(struct sigaction) {SIG_IGN}, NULL); base_path_len = strlen(BASE_PATH); s = socket(AF_INET, SOCK_STREAM, 0); if (s < 0) { printf("Couldn't create the socket\n"); return s; } memset(&srv_addr, 0, sizeof(srv_addr)); srv_addr.sin_family = AF_INET; srv_addr.sin_addr.s_addr = htonl(INADDR_ANY); srv_addr.sin_port = htons(SRV_PORT); r = bind(s, (struct sockaddr *) &srv_addr, sizeof(srv_addr)); if (r < 0) { fprintf(stderr, "bind() failed\n"); return r; } r = listen(s, 128); if (r < 0) { fprintf(stderr, "listen() failed\n"); return r; } pthread_attr_init(&th_attr); pthread_attr_setdetachstate(&th_attr, PTHREAD_CREATE_DETACHED); r = pthread_attr_setstacksize(&th_attr, PTHREAD_STACK_MIN); if (r < 0) { fprintf(stderr, "setstacksize() failed!\n"); return r; } while (1) { pthread_t th; struct th_data *data; socklen_t sin_size = sizeof(struct sockaddr_in); int conn_fd = accept(s, (struct sockaddr *) &client_addr, &sin_size); if (conn_fd < 0) continue; data = malloc(sizeof(*data)); if (!data) { fprintf(stderr, "Could not allocate th_data struct\n"); close(conn_fd); continue; } data->conn_fd = conn_fd; data->thread_id = ++thread_id; r = pthread_create(&th, &th_attr, handle_connection, data); if (r < 0) { fprintf(stderr, "Could not start the thread\n"); free(data); close(conn_fd); } else { printf("thread %u: Serving client %s\n", thread_id, inet_ntoa(client_addr.sin_addr)); } } return 0; } static void* handle_connection(void *data) { struct th_data *th_data = data; int conn_fd = th_data->conn_fd; int r; unsigned const th_id = th_data->thread_id; struct timeval timeout; char *buf = NULL; printf("thread %u: starting\n", th_id); timeout.tv_sec = 3600; timeout.tv_usec = 0; buf = malloc(BUFSIZE); if (!buf) { fprintf(stderr, "thread %u: Could not allocate the buffer\n", th_id); goto exit; } if (setsockopt(conn_fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)) < 0) { fprintf(stderr, "thread %u: Could not set RECV timeout\n", th_id); goto exit; } if (setsockopt(conn_fd, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout)) < 0) { fprintf(stderr, "thread %u: Could not set SEND timeout\n", th_id); goto exit; } while (1) { r = read(conn_fd, buf, BUFSIZE - 1); if (r < 0) { fprintf(stderr, "thread %u: read() failed, exit\n", th_id); goto exit; } else if (r == 0) { printf("thread %u: Client disconnected\n", th_id); goto exit; } r = handle_request(buf, r, conn_fd, th_id); if (r < 0) { /* Close the connection */ goto exit; } } exit: printf("thread %u: terminating\n", th_id); close(conn_fd); free(data); free(buf); pthread_exit((void *) 0); } static int handle_request(char *req, int len, int conn_fd, unsigned th_id) { int r = -1, i; char *token, *next_tok; char *args[3]; char *content; char const *delim = {" \n"}; req[len] = 0; if (len < 14 /*GET / HTTP/1.x is the shortest command we support*/ ) { fprintf(stderr, "thread %u: request too short\n", th_id); goto err; } next_tok = req; for (i = 0; i < 3; ++i) { token = strsep(&next_tok, delim); if (!token) { fprintf(stderr, "thread %u: received a malformed request\n", th_id); goto err; } args[i] = token; } content = next_tok; if (strncmp(args[2], "HTTP/1.1", 8) && strncmp(args[2], "HTTP/1.0", 8)) { fprintf(stderr, "thread %u: unsupported protocol %s\n", th_id, args[2]); goto err; } switch (req_type(args[0])) { case REQ_TYPE_GET: r = handle_request_get(args[1], conn_fd, th_id, content); break; case REQ_TYPE_HEAD: r = handle_request_head(conn_fd); break; case REQ_TYPE_POST: /* printf("%s %s %s\n%s\n", args[0], args[1], args[2], content); */ /*Not supported yet*/ break; default: fprintf(stderr, "thread %u: unsupported request %s\n", th_id, args[0]); goto err; } if (r < 0) fprintf(stderr, "thread %u: failed to execute the %s request\n", th_id, args[0]); err: return r; } static struct content_range * get_content_range(char *req) { struct content_range *range = NULL; size_t start = 0, end = 0; char *r; if (!req) goto exit; r = strstr(req, "Range:"); if (!r) goto exit; r = strstr(r, "bytes="); if (!r) goto exit; r += 6; start = strtoll(r, NULL, 10); r = strstr(r, "-"); if (!r) goto exit; r++; end = strtoll(r, NULL, 10); range = malloc(sizeof(*range)); if (!range) goto exit; range->start = start; range->end = end; exit: return range; } static int handle_request_head(int conn_fd) { char header[512]; int header_len; header_len = snprintf(header, sizeof(header), "HTTP/1.1 200 OK\r\n" "Server: nanoCwebsrv/0.2 (FreeBSD)\r\n" "Content-Length: 0\r\n" "Content-Type: text/html\r\n\r\n"); return write(conn_fd, header, header_len); } static int handle_request_get(char *fname, int conn_fd, unsigned th_id, char *content) { char *file_path = NULL; char *file_type; int r = -1; int retried = 0; int file_fd = -1; struct stat file_stat; struct content_range *range = NULL; if (strstr(fname, "..")) { /* Block malicious GET requests */ fprintf(stderr, "thread %u: blocked suspicios GET req\n", th_id); goto err; } if (fname[0] == '/' && fname[1] == 0) fname = "/index.html"; range = get_content_range(content); if (range) printf("thread %u: got range start = %u end = %u\n", th_id, range->start, range->end); retry: asprintf(&file_path, "%s/%s", BASE_PATH, fname); if (!file_path) { fprintf(stderr, "thread %u: cannot allocate the file path string\n", th_id); goto err; } printf("thread %u: accessing file %s\n", th_id, file_path); file_fd = open(file_path, O_RDONLY); if (file_fd < 0) { fprintf(stderr, "thread %u: File %s not found... 404\n", th_id, file_path); if (retried) goto err; fname = "/404.html"; retried = 1; free(file_path); free(range); range = NULL; goto retry; } r = fstat(file_fd, &file_stat); if (r < 0) { fprintf(stderr, "thread %u: Failed to get the %s file size\n", th_id, file_path); goto err; } switch (file_stat.st_mode & S_IFMT) { case S_IFDIR: if (file_path[strlen(file_path) - 1] != '/') { char *dir_addr; /* We should append a / char to make directories work */ asprintf(&dir_addr, "%s%s/", WEBSITE_ADDR, fname); if (!dir_addr) { r = -1; goto err; } r = send_moved_document_page(dir_addr, conn_fd); free(dir_addr); } else { /* Create a list of the directory's content */ r = send_dir_to_client(file_path, conn_fd); } break; case S_IFREG: file_type = filename_to_contenttype(file_path); r = send_file_to_client(file_fd, conn_fd, file_stat.st_size, file_type, range); if (r < 0) { fprintf(stderr, "thread %u: cannot send %s to client\n", th_id, file_path); goto err; } break; default: fprintf(stderr, "thread %u: Unsupported file type\n", th_id); r = -1; goto err; } err: if (file_fd >= 0) close(file_fd); free(file_path); free(range); return r; } static int send_dir_to_client(char *dir_path, int conn_fd) { DIR *d; struct dirent *dentry; char *dir_name; int r = -1; unsigned dlen = 0; unsigned buflen = 4096; char *buf = NULL, *delim; char header[256]; unsigned hlen; char *path_cp = NULL; char *parent_dir_path; int l; d = opendir(dir_path); if (!d) return -1; buf = malloc(buflen); if (!buf) goto exit; path_cp = strdup(dir_path); if (!path_cp) goto exit; dir_name = basename(path_cp); parent_dir_path = &dirname(path_cp)[base_path_len]; if ((l = strlen(parent_dir_path)) == 0) parent_dir_path = "/"; else parent_dir_path++; /* Get rid of the initial / char */ dlen = snprintf(buf, buflen, "" "Index of %s/\r\n" "\r\n" "

Index of %s

\r\n" "
\r\n" "\r\n" "" "\r\n" "\r\n" "\r\n", dir_name, dir_name, parent_dir_path, l == 0 ? "" : "/"); while ((dentry = readdir(d))) { char filepath[4096]; char size[64]; char mtime[64]; char *filetype; struct stat file_stat; if (dentry->d_name[0] == '.') continue; /* Grow the buffer if space is running low */ if (buflen - dlen < 512) { char *newbuf; buflen += 4096; newbuf = realloc(buf, buflen); if (!newbuf) goto exit; buf = newbuf; } if (dentry->d_type & DT_DIR) { filetype = "Directory"; delim = "/"; size[0] = mtime[0] = '-'; size[1] = mtime[1] = 0; } else { filetype = filename_to_contenttype(dentry->d_name); delim = ""; snprintf(filepath, sizeof(filepath), "%s/%s", dir_path, dentry->d_name); if (stat(filepath, &file_stat) < 0) goto exit; strftime(mtime, sizeof(mtime), "%Y-%m-%d %H:%M:%S", localtime(&file_stat.st_mtime)); snprintf(size, sizeof(size), "%lld", (unsigned long long)file_stat.st_size); } dlen += snprintf(&buf[dlen], buflen - dlen, "" "" "" "" "\r\n", dentry->d_name, delim, dentry->d_name, delim, mtime, size, filetype); } dlen += snprintf(&buf[dlen], buflen - dlen, "
NameLast ModifiedSizeType
../-  " "-  Directory
%s%s%s%s%s
\r\n"); hlen = snprintf(header, sizeof(header), "HTTP/1.1 200 OK\r\n" "Content-Length: %u\r\n" "Content-Type: text/html\r\n\r\n", dlen); r = write(conn_fd, header, hlen); if (r < 0) goto exit; r = write(conn_fd, buf, dlen); if (r < 0) goto exit; r = 0; exit: free(buf); free(path_cp); closedir(d); return r; } static int send_file_to_client(int file_fd, int conn_fd, size_t file_size, char *file_type, struct content_range *range) { char *header, *fmap; int r = -1; int hd_len; size_t idx = 0; size_t end = file_size; ssize_t wb; if (range == NULL) { hd_len = asprintf(&header, "HTTP/1.1 200 OK\r\nContent-Length: %u\r\nContent-Type: %s\r\n\r\n", (unsigned) file_size, file_type); } else { if (range->end == 0) range->end = file_size - 1; if (range->start > range->end) return -1; if (range->end >= file_size) return -1; idx = range->start; end = range->end + 1; hd_len = asprintf(&header, "HTTP/1.1 206 Partial Content\r\n" "Content-Range: bytes %u-%u/%u\r\n" "Content-Length: %u\r\n" "Content-Type: %s\r\n\r\n", range->start, range->end, file_size, range->end - range->start + 1, file_type); } if (!header) return -1; fmap = mmap(NULL, file_size, PROT_READ, MAP_PRIVATE | MAP_PREFAULT_READ, file_fd, 0); if (fmap == MAP_FAILED) goto exit; if (write(conn_fd, header, hd_len) < 0) goto exit; while (idx < end) { wb = write(conn_fd, fmap + idx, end - idx); if (wb < 0) goto exit; idx += wb; } r = 0; exit: if (fmap != MAP_FAILED) munmap(fmap, file_size); free(header); return r; } static int send_moved_document_page(char *dest, int conn_fd) { char header[256]; char *page = NULL; int page_len, header_len; int r = -1; page_len = asprintf(&page, "\r\n" "301 Moved Permanently\r\n" "\r\n" "

Moved Permanently

" "

The document has moved here.

\r\n" "", dest); if (!page) goto exit; header_len = snprintf(header, sizeof(header), "HTTP/1.1 301 Moved Permanently\r\n" "Location: %s\r\n" "Content-Length: %d\r\n" "Content-Type: text/html\r\n\r\n", dest, page_len); r = write(conn_fd, header, header_len); if (r < 0) goto exit; r = write(conn_fd, page, page_len); exit: free(page); return r; } static int req_type(char const *req) { if (!strncmp(req, "GET", 3)) return REQ_TYPE_GET; else if (!strncmp(req, "HEAD", 4)) return REQ_TYPE_HEAD; else if (!strncmp(req, "POST", 4)) return REQ_TYPE_POST; else return REQ_TYPE_UNSUP; } static char *filename_to_contenttype(char *fname) { char ext[16] = {0}; int i; char *fname_ext = strrchr(fname, '.'); if (fname_ext == NULL) goto octet; strncat(ext, fname_ext, sizeof(ext) - 1); for (i = 0; ext[i]; ++i) ext[i] = tolower(ext[i]); for (i = 0; fname_type_table[i].ext; ++i) { if (!strcmp(ext, fname_type_table[i].ext)) return fname_type_table[i].ctype; } octet: return "application/octet-stream"; }