/* __ _ * ____ / /_ ____ ___ __ ___________ _(_) * / __ \/ __ \/ __ `__ \/ / / / ___/ __ `/ / * / /_/ / / / / / / / / / /_/ / /__/ /_/ / / * \____/_/ /_/_/ /_/ /_/\__, /\___/\__, /_/ * /____/ /____/ * * SPDX-License-Identifier: BSD-2-Clause-FreeBSD * * Copyright (c) 2021, Max Christian Pohle * * 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. * * {{{ DISCLAIMER * 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 OWNER 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. * }}} */ // {{{ INCLUDES #include #include #include #include #include #include #include #include #include #include #include // #include // maybe later // }}} // {{{ MACROS #define EWOULDBLOCK_DELAY 100 #define READ_BUFFER_LENGTH 9000 // jumboframe? #define POST_DATA_MAX_LENGTH 18000 #define DEBUG_SLEEP_TIME 50000 #ifndef DEBUG #define DEBUG(X, ...) // (X, ...) #else #include static inline int verbose(const char * format, ...) { va_list va; va_start(va, format); usleep(DEBUG_SLEEP_TIME); return vprintf(format, va); } #undef DEBUG #define DEBUG verbose #endif // }}} typedef struct { int newline_length; // lenght of one newline in bytes (\n has 1, CR/LF has 2) char * method; // GET/POST or something like that char * url; // request URL (e.g. /index.html) char * boundary; // usually looks similar to ------1234 size_t boundary_size; // size in bytes, calculated after first header complete; is an indicator for the first header char * content_type; // sometimes 'text/html', also sometimes 'text/html; boundary=------1234' char * content_disposition; // includes file names of uploaded files or field names with form-data (e.g. curl -F) } Http_Header; void send_answer(int fd_socket, Http_Header * http_header) { FILE * f = fdopen((size_t) fd_socket, "w"); fputs("HTTP/1.0 200 OK\n", f); fputs("content-type: text/plain\n\n", f); fflush(f); int file = open(&http_header->url[1], O_RDONLY); if(0 < file) { struct stat stat; fstat(file, &stat); sendfile (fileno(f), file, NULL, stat.st_size); } else { if(http_header->url) // TODO: too dangerous to check that here, that is too late. fprintf(f, "could not open file \"%s\"\n", &http_header->url[1]); } fclose(f); } int read_everything(FILE * f_r, FILE * output) { const int read_buffer_length = READ_BUFFER_LENGTH; char read_buffer[read_buffer_length]; for(size_t size = -2 ; ; size = fread(read_buffer, 1, read_buffer_length, f_r)) { if(-2 == size || (-1 == size && EWOULDBLOCK == errno)) { usleep(EWOULDBLOCK_DELAY); // try again a little later continue; } fwrite(read_buffer, read_buffer_length, 1, output); if (read_buffer_length > POST_DATA_MAX_LENGTH) return EXIT_FAILURE; if (size < read_buffer_length) { // I expect one nmemb of data break; } } fflush(output); return EXIT_SUCCESS; } void * next_customer(size_t new_socket) { FILE * f_r = fdopen((size_t) new_socket, "r"); char * output_buffer = NULL; size_t output_buffer_length = 0; FILE * output = open_memstream(&output_buffer, &output_buffer_length); read_everything(f_r, output); // TODO: catch return value and error handling shutdown(new_socket, SHUT_RD); // shutdown the reading half of the connection char * start = output_buffer; char * end = NULL; char * search = "\r\n"; Http_Header http_header = {0}; char * name = NULL; while(NULL != (end = strpbrk(start, search))) { // TODO: try harder to break things (are SEGFAULTs possible?) size_t matchlen = strspn(end, search); switch(end[0]) { case ':': end[0] = '\0'; // {{{ remember header 'names' and search for the value end++; // jump over the colon name = start; // remember, where name starts, will be important in the newline case if (0 == strcasecmp("Content-Type", start)) { search = "\r\n;"; // (more unlikely) also search for a semicolon in Content-Type: [...]; boundary=[...] } else { search = "\r\n"; // (likely) search for some kind of newline } // }}} break; case ';': // {{{ find the form-data boundary in the main header start += strspn(start, "; "); // remove spaces and semicolons (boundary check implicit; also stops at '\0') const char s_multipart_form_data[] = "boundary="; if(NULL == http_header.boundary && 0 < strcasecmp(start, s_multipart_form_data)) { http_header.boundary = end + sizeof(s_multipart_form_data) + 1; http_header.boundary += strspn(http_header.boundary, "-"); DEBUG("> Boundary found, now looking where it ends...\n"); search = "\r\n"; continue; } /// }}} break; case '\r': // fallthrough case '\n': // {{{ newlines are special: sometimes content parts follow and sometimes headers, guess what... end[0] = '\0'; search = ":"; // we will continue to search for headers if(NULL == name) { if(NULL == http_header.method) { DEBUG("[%ld]> HTTP REQUEST LINE :: %s \n", matchlen, start); end[0] = '\0'; while(NULL != (start = memchr(start, ' ', end - start))) { if(NULL == http_header.url) http_header.url = ++start; else start[0] = '\0'; } http_header.method = start; http_header.newline_length = matchlen; } else { DEBUG("[...]\n"); // if we want to intentially skip something, we land here by setting name = NUL; break; } } else { // we know that name is not NULL and can work with it if (0 == strcasecmp("Content-Disposition", name)) { http_header.content_disposition = start; } } // }}} DEBUG("\033[32m[%ld]> '% 20s' = '%s'\033[0m\n", matchlen, name, start); // {{{ check if a http header ended (e.g. two newlines) if(matchlen > http_header.newline_length) { DEBUG("> END HEADERS, because there were %d newlines; boundary='%s'[%ld]\n", matchlen / http_header.newline_length, http_header.boundary, http_header.boundary_size); end += matchlen; // if it was the first header, we calculate the boundary size and expect more headers to come after a boundary if(http_header.boundary && http_header.boundary_size == 0) { DEBUG("================================================================================\n"); http_header.boundary_size = strlen(http_header.boundary); // skip the first header and boundary... start = end; start += strspn(start, "-"); start += http_header.boundary_size; start += http_header.newline_length; continue; } else { char * content_start = end; while(1) { size_t size_remaining = (size_t) output_buffer_length - (end - output_buffer) - 1; DEBUG("%ld remaining.\n", size_remaining); if(size_remaining <= 0) { DEBUG("> not even the boundary would fit in that what is left.\n"); break; } if(NULL == (end = memchr((void*) end, '-', size_remaining))) { DEBUG("no further '-' found\n"); break; } char * content_end = end - http_header.newline_length; end += strspn(end, "-"); if(0 == strncmp(end, http_header.boundary, http_header.boundary_size)) { size_t file_size = content_end - content_start; DEBUG("> Content ends here, size of the last file is %ld.", file_size); end += http_header.boundary_size; matchlen = strspn(end, "\r\n"); DEBUG("> end is at %p, matchlen is %ld\n", end, matchlen); search = ":"; break; } else { end = end + 1; } } } break; } // }}} if condition after a header } // switch if(NULL == end) break; else start = end + matchlen; } DEBUG("> sending answer..."); send_answer(new_socket, &http_header); DEBUG("> answer sent."); fclose(f_r); fclose(output); free(output_buffer); return NULL; } int serve(int server_fd) { struct sockaddr_in address; socklen_t address_len = sizeof(address); DEBUG("> Waiting for connections on server file descriptor %d\n", server_fd); size_t new_socket = -1; while(-1 != (new_socket = accept(server_fd, (struct sockaddr*) &address, &address_len))) { DEBUG("> Client %ld is connected via port %d\n", new_socket, address.sin_port); // set non blocking mode... fcntl( (size_t) new_socket, F_SETFL, fcntl((size_t) new_socket, F_GETFL) | O_NONBLOCK ); next_customer(new_socket); #ifdef VALGRIND break; // only run once, so that valgrind can test allocations&frees #endif } err(errno, "error serving"); close(server_fd); } int main(const int argc, char const * argv[]) { int server_fd = -1, opt = 1; int port = atoi(argc > 1 ? argv[1] : "8080"); 0 == port ? port = 8080 : port; struct sockaddr_in address = { .sin_family = AF_INET, .sin_addr.s_addr = INADDR_ANY, .sin_port = htons(port) }; // I <3 C 0 == (server_fd = socket(AF_INET, SOCK_STREAM, 0)) ? err(errno, NULL) : setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt)) ? err(errno, "setsockopt failed on socket with fileno %d", server_fd) : bind(server_fd, (struct sockaddr*) &address, sizeof(address)) ? err(errno, NULL) : listen(server_fd, SOMAXCONN) ? err(errno, NULL) : serve(server_fd) ? err(errno, NULL) : exit(EXIT_SUCCESS) ; return EXIT_FAILURE; } // vim: shiftwidth=2 tabstop=2 number foldmethod=marker