├── README.md ├── LICENSE ├── Makefile ├── simple_http_file_server_main.cc ├── simple_http_file_server_handler.h └── simple_http_file_server_handler.cc /README.md: -------------------------------------------------------------------------------- 1 | # cc-simplehttpfileserver 2 | 3 | This repository contains the `cc-simplehttpfileserver` library. 4 | 5 | It is a highly efficient, lightweight, basic http 1.1 fileserver that lives behind a proxy 6 | and can efficiently serve static files using `sendfile` and `epoll`. 7 | 8 | It eliminates the requirement to include `nginx` in your stack just to serve static files behind 9 | a load-balancer or other reverse proxy. 10 | 11 | ## Building 12 | 13 | ```sh 14 | make 15 | ``` 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Ugorji Nwoke 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | COMMON = $(CURDIR)/../cc-common 2 | 3 | include $(COMMON)/Makefile.include.mk 4 | 5 | objFiles = \ 6 | $(BUILD)/simple_http_file_server_handler.o \ 7 | $(BUILD)/simple_http_file_server_main.o \ 8 | 9 | all: .common.all $(objFiles) $(BUILD)/__simplehttpfileserver $(BUILD)/__simplehttpfileserver_static 10 | 11 | clean: 12 | rm -f $(BUILD)/* 13 | 14 | $(BUILD)/__simplehttpfileserver: $(commonObjFiles) $(objFiles) | $(BUILD) 15 | mkdir -p $(BUILD) && \ 16 | $(CXX) -o $(BUILD)/__simplehttpfileserver $(commonObjFiles) $(objFiles) $(LDFLAGS) 17 | 18 | $(BUILD)/__simplehttpfileserver_static: $(commonObjFiles) $(objFiles) | $(BUILD) 19 | mkdir -p $(BUILD) && \ 20 | $(CXX) -o $(BUILD)/__simplehttpfileserver_static $(commonObjFiles) $(objFiles) $(LDFLAGS) -static -static-libgcc -static-libstdc++ 21 | 22 | server: $(BUILD)/__simplehttpfileserver $(BUILD)/__simplehttpfileserver_static 23 | ulimit -c unlimited && \ 24 | $(BUILD)/__simplehttpfileserver -p 9999 -w -1 -d localhost:9999 -m /s /home/ugorji/Downloads 25 | 26 | server.static: $(BUILD)/__simplehttpfileserver $(BUILD)/__simplehttpfileserver_static 27 | ulimit -c unlimited && \ 28 | $(BUILD)/__simplehttpfileserver_static -p 9999 -w -1 -d localhost:9999 -m /s /home/ugorji/Downloads 29 | 30 | server.valgrind: $(BUILD)/__simplehttpfileserver $(BUILD)/__simplehttpfileserver_static 31 | ulimit -c unlimited && \ 32 | valgrind -s --track-origins=yes --leak-check=full --show-reachable=yes \ 33 | $(BUILD)/__simplehttpfileserver -p 9999 -w -1 -d localhost:9999 -m /s /home/ugorji/Downloads 34 | 35 | server.massif: $(BUILD)/__simplehttpfileserver $(BUILD)/__simplehttpfileserver_static 36 | ulimit -c unlimited && \ 37 | valgrind -s --tool=massif \ 38 | $(BUILD)/__simplehttpfileserver -p 9999 -w -1 -d localhost:9999 -m /s /home/ugorji/Downloads 39 | 40 | -------------------------------------------------------------------------------- /simple_http_file_server_main.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include "simple_http_file_server_handler.h" 11 | #include 12 | 13 | ugorji::conn::Manager* mgr_; 14 | 15 | int main(int argc, char** argv) { 16 | ugorji::util::Log::getInstance().minLevel_ = ugorji::util::Log::TRACE; 17 | int port = 9999; 18 | int workers = -1; 19 | std::unordered_map*> mappings; 20 | std::map* map; 21 | std::vector>> maps; 22 | std::vector domains; 23 | 24 | bool doMap(false); 25 | for(int i = 1; i < argc; i++) { 26 | std::string arg = argv[i]; 27 | if(arg == "-p" || arg == "--port") { 28 | port = std::stoi(argv[++i]); 29 | } else if(arg == "-w" || arg == "--workers") { 30 | workers = std::stoi(argv[++i]); 31 | } else if(arg == "-d" || arg == "--domain" || arg == "--host") { 32 | doMap = true; 33 | domains.push_back(std::string(argv[++i])); 34 | } else if(arg == "-m" || arg == "--mapping") { 35 | if(doMap) { 36 | LOG(TRACE, " mapping for some domains", 0); 37 | auto mm = std::make_unique>(); 38 | map = mm.get(); 39 | maps.push_back(std::move(mm)); 40 | for(auto& d: domains) mappings[d] = map; 41 | domains.clear(); 42 | doMap = false; 43 | } 44 | auto k = std::string(argv[++i]); 45 | auto v = std::string(argv[++i]); 46 | LOG(TRACE, " mapping %s => %s", k.data(), v.data()); 47 | (*map)[k] = v; 48 | } else if(arg == "-h" || arg == "--help") { 49 | std::cout << "Usage: simplehttpfileserver " << std::endl 50 | << "\t[-p|--port portno] Default: 9999" << std::endl 51 | << "\t[-w|--workers numWorkers] Default: -1 (unlimited)" << std::endl 52 | << "\t[-d|--domain domain] ..." << std::endl 53 | << "\t[-m|--mapping prefix substitution] ..." << std::endl 54 | << std::endl; 55 | return 0; 56 | } 57 | } 58 | 59 | LOG(INFO, " port: %d", port); 60 | for(auto& m: mappings) { 61 | LOG(TRACE, " domain: '%s' (size: %d)", m.first.data(), m.second->size()); 62 | for(auto& m2: *(m.second)) LOG(TRACE, " \t%s => %s", m2.first.data(), m2.second.data()); 63 | } 64 | 65 | //always install signal handler in main thread, and before making other threads. 66 | LOG(INFO, " Setup Signal Handler (SIGINT, SIGTERM, SIGUSR1)", 0); 67 | 68 | auto sighdlr = [](int sig) { 69 | LOG(INFO, " receiving signal: %d", sig); 70 | if(mgr_ != nullptr) { 71 | mgr_->close(); 72 | mgr_ = nullptr; 73 | } 74 | }; 75 | auto sighdlr_noop = [](int sig) {}; 76 | std::signal(SIGINT, sighdlr); // ctrl-c 77 | std::signal(SIGTERM, sighdlr); // kill 78 | std::signal(SIGUSR1, sighdlr_noop); // used to interrupt epoll_wait 79 | // std::signal(SIGUSR2, sighdlr_noop); // used to interrupt epoll_wait 80 | 81 | auto mgr = std::make_unique(port, workers); 82 | mgr_ = mgr.get(); 83 | std::string err = ""; 84 | mgr->open(err); 85 | if(err.size() > 0) { 86 | LOG(ERROR, "%s", err.data()); 87 | return 1; 88 | } 89 | 90 | std::vector empty; 91 | std::vector> hdlrs; 92 | auto fn = [&]() mutable -> decltype(auto) { 93 | auto hh = std::make_unique(mappings, empty, empty); 94 | auto hdlr = hh.get(); 95 | hdlrs.push_back(std::move(hh)); 96 | return *hdlr; 97 | }; 98 | 99 | mgr->run(fn, true); 100 | mgr->wait(); 101 | 102 | int exitcode = (mgr->hasServerErrors() ? 1 : 0); 103 | 104 | LOG(INFO, "
: shutdown completed", 0); 105 | 106 | return exitcode; 107 | } 108 | -------------------------------------------------------------------------------- /simple_http_file_server_handler.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | class simpleHttpFileServerConnFdStateMach { 6 | public: 7 | std::mutex mu_; 8 | slice_bytes in_; 9 | slice_bytes resp_; // this is the header and body, except 10 | int fd_; 11 | int out_cursor_; 12 | int in_cursor_; 13 | int path_fd_; 14 | int resp_size_; 15 | off_t sendfile_offset_; 16 | std::time_t resp_mod_time_; 17 | std::string_view path_; 18 | slice_bytes_t host_; 19 | slice_bytes_t if_mod_since_; 20 | slice_bytes_t if_unmod_since_; 21 | ugorji::conn::ConnState state_; 22 | bool resp_ok_; 23 | bool resp_head_only_; 24 | 25 | explicit simpleHttpFileServerConnFdStateMach(int fd) : 26 | in_({}), 27 | resp_({}), 28 | fd_(fd) 29 | { 30 | // slice_bytes_zero(&in_); 31 | // slice_bytes_zero(&resp_); 32 | reinit(); 33 | } 34 | ~simpleHttpFileServerConnFdStateMach() { 35 | slice_bytes_free(in_); 36 | slice_bytes_free(resp_); 37 | } 38 | // previously, we use std::string.append(...) but std::string involves a lot 39 | // of copying. Instead, we put append directly on the State machine. 40 | // This just appends some bytes onto the response. 41 | // 42 | // Note that a std::span would have been a good alternative. 43 | simpleHttpFileServerConnFdStateMach& append(const std::string_view& s) { 44 | slice_bytes_append(&resp_, s.data(), s.size()); 45 | return *this; 46 | } 47 | void writeDone() { 48 | out_cursor_ = 0; 49 | resp_.bytes.len = 0; 50 | resp_size_ = 0; 51 | resp_ok_ = false; 52 | resp_head_only_ = false; 53 | host_ = {}; 54 | if_mod_since_ = {}; 55 | if_unmod_since_ = {}; 56 | resp_mod_time_ = 0; 57 | state_ = ugorji::conn::CONN_READY; 58 | path_fd_ = -1; 59 | sendfile_offset_ = 0; 60 | path_ = ""; // path_.clear(); 61 | } 62 | void reinit() { 63 | in_.bytes.len = 0; 64 | in_cursor_ = 0; 65 | writeDone(); 66 | } 67 | }; 68 | 69 | // SimpleHttpFileServerHandler 70 | // 71 | // parse the http 1.1 request, get the path, do the path mapping translation, 72 | // find the file, send it out with headers including lastmod and content-length, 73 | // or send out an appropriate 4XX error code (if not exist, cannot read, etc). 74 | // 75 | // The only HTTP methods supported are GET and HEAD. 76 | // 77 | // Expects requests with an absolute path, and a Host header specified. 78 | // Honors If-Modified-ince and If-Unmodified-Since flags. 79 | // 80 | // This server also supports pipelining of requests, so multiple requests can be 81 | // sent on a persistent connection before any response is sent (in order). 82 | // 83 | // all gzip, parsing, etc is done by reverse proxy. 84 | // 85 | // There is only support for regular files, not sym links or directories. 86 | // 87 | // This also supports multiple Hosts, with distinct mappings for each one. 88 | // e.g. 89 | // ugorji.net: 90 | // /s --> /.../ 91 | // / --> /.../ 92 | // nwoke.net: 93 | // /s --> /.../ 94 | // / --> /.../ 95 | // This way, multiple hosts can have their files served from a single instance. 96 | class SimpleHttpFileServerHandler : public ugorji::conn::Handler { 97 | private: 98 | std::mutex mu_; 99 | char errbuf_[128] {}; 100 | // ugorji::conn::ConnState state_; 101 | std::unordered_map> clientfds_; 102 | std::unordered_map*>& path_mappings_; 103 | std::vector& deny_starts_with_; 104 | std::vector& deny_ends_with_; 105 | simpleHttpFileServerConnFdStateMach& stateFor(int fd); 106 | void readFd(simpleHttpFileServerConnFdStateMach& h, std::string& err); 107 | void processFd(simpleHttpFileServerConnFdStateMach& h, std::string& err); 108 | void writeFd(simpleHttpFileServerConnFdStateMach& h, std::string& err); 109 | public: 110 | SimpleHttpFileServerHandler(std::unordered_map*>& path_mappings, 111 | std::vector& deny_starts_with, 112 | std::vector& deny_ends_with) : 113 | Handler(), 114 | path_mappings_(path_mappings), 115 | deny_starts_with_(deny_starts_with), 116 | deny_ends_with_(deny_ends_with) {} 117 | ~SimpleHttpFileServerHandler() {} 118 | void handleFd(int fd, std::string& err) override; 119 | void unregisterFd(int fd, std::string& err) override; 120 | }; 121 | 122 | -------------------------------------------------------------------------------- /simple_http_file_server_handler.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include 11 | #include 12 | 13 | #include 14 | #include 15 | 16 | #include 17 | 18 | #include "simple_http_file_server_handler.h" 19 | 20 | const int CONN_BUF_INCR = 256; // ugorji::conn::CONN_BUF_INCR 21 | 22 | // TODO 23 | // - consider sending in chunks of 16Kib each (default socket size) 24 | // per https://www.nginx.com/blog/thread-pools-boost-performance-9x/ 25 | // - cap sendfile to 16MB, 26 | // per https://serverfault.com/questions/666214/slow-download-big-static-files-from-nginx/812737 27 | // 28 | // const size_t CONN_SENDFILE_CHUNK_SIZE = 1 << 14; // 16Kib 29 | 30 | std::string view2cstr(std::string_view& s) { 31 | // LOG(TRACE, " size: %d", s.size()); 32 | return std::string(s.data(), s.size()); 33 | } 34 | 35 | std::string bytes2cstr(slice_bytes_t& c) { 36 | return std::string(c.v, c.len); 37 | } 38 | 39 | // retrofitted from https://raw.githubusercontent.com/nginx/nginx/master/conf/mime.types 40 | // 41 | // got by running: 42 | // cd ~/repos/local/repo/go/src/ugorji.net/scratch/main 43 | // curl -s https://raw.githubusercontent.com/nginx/nginx/master/conf/mime.types | 44 | // go run -tags nginx_mime nginx_mime_types_to_cpp_map.go 45 | const std::unordered_map MIME_TYPES = 46 | { 47 | {"html", "text/html"}, 48 | {"htm", "text/html"}, 49 | {"shtml", "text/html"}, 50 | {"css", "text/css"}, 51 | {"xml", "text/xml"}, 52 | {"gif", "image/gif"}, 53 | {"jpeg", "image/jpeg"}, 54 | {"jpg", "image/jpeg"}, 55 | {"js", "application/javascript"}, 56 | {"atom", "application/atom+xml"}, 57 | {"rss", "application/rss+xml"}, 58 | 59 | {"mml", "text/mathml"}, 60 | {"txt", "text/plain"}, 61 | {"jad", "text/vnd.sun.j2me.app-descriptor"}, 62 | {"wml", "text/vnd.wap.wml"}, 63 | {"htc", "text/x-component"}, 64 | 65 | {"png", "image/png"}, 66 | {"svg", "image/svg+xml"}, 67 | {"svgz", "image/svg+xml"}, 68 | {"tif", "image/tiff"}, 69 | {"tiff", "image/tiff"}, 70 | {"wbmp", "image/vnd.wap.wbmp"}, 71 | {"webp", "image/webp"}, 72 | {"ico", "image/x-icon"}, 73 | {"jng", "image/x-jng"}, 74 | {"bmp", "image/x-ms-bmp"}, 75 | 76 | {"woff", "font/woff"}, 77 | {"woff2", "font/woff2"}, 78 | 79 | {"jar", "application/java-archive"}, 80 | {"war", "application/java-archive"}, 81 | {"ear", "application/java-archive"}, 82 | {"json", "application/json"}, 83 | {"hqx", "application/mac-binhex40"}, 84 | {"doc", "application/msword"}, 85 | {"pdf", "application/pdf"}, 86 | {"ps", "application/postscript"}, 87 | {"eps", "application/postscript"}, 88 | {"ai", "application/postscript"}, 89 | {"rtf", "application/rtf"}, 90 | {"m3u8", "application/vnd.apple.mpegurl"}, 91 | {"kml", "application/vnd.google-earth.kml+xml"}, 92 | {"kmz", "application/vnd.google-earth.kmz"}, 93 | {"xls", "application/vnd.ms-excel"}, 94 | {"eot", "application/vnd.ms-fontobject"}, 95 | {"ppt", "application/vnd.ms-powerpoint"}, 96 | {"odg", "application/vnd.oasis.opendocument.graphics"}, 97 | {"odp", "application/vnd.oasis.opendocument.presentation"}, 98 | {"ods", "application/vnd.oasis.opendocument.spreadsheet"}, 99 | {"odt", "application/vnd.oasis.opendocument.text"}, 100 | 101 | {"wmlc", "application/vnd.wap.wmlc"}, 102 | {"7z", "application/x-7z-compressed"}, 103 | {"cco", "application/x-cocoa"}, 104 | {"jardiff", "application/x-java-archive-diff"}, 105 | {"jnlp", "application/x-java-jnlp-file"}, 106 | {"run", "application/x-makeself"}, 107 | {"pl", "application/x-perl"}, 108 | {"pm", "application/x-perl"}, 109 | {"prc", "application/x-pilot"}, 110 | {"pdb", "application/x-pilot"}, 111 | {"rar", "application/x-rar-compressed"}, 112 | {"rpm", "application/x-redhat-package-manager"}, 113 | {"sea", "application/x-sea"}, 114 | {"swf", "application/x-shockwave-flash"}, 115 | {"sit", "application/x-stuffit"}, 116 | {"tcl", "application/x-tcl"}, 117 | {"tk", "application/x-tcl"}, 118 | {"der", "application/x-x509-ca-cert"}, 119 | {"pem", "application/x-x509-ca-cert"}, 120 | {"crt", "application/x-x509-ca-cert"}, 121 | {"xpi", "application/x-xpinstall"}, 122 | {"xhtml", "application/xhtml+xml"}, 123 | {"xspf", "application/xspf+xml"}, 124 | {"zip", "application/zip"}, 125 | 126 | {"bin", "application/octet-stream"}, 127 | {"exe", "application/octet-stream"}, 128 | {"dll", "application/octet-stream"}, 129 | {"deb", "application/octet-stream"}, 130 | {"dmg", "application/octet-stream"}, 131 | {"iso", "application/octet-stream"}, 132 | {"img", "application/octet-stream"}, 133 | {"msi", "application/octet-stream"}, 134 | {"msp", "application/octet-stream"}, 135 | {"msm", "application/octet-stream"}, 136 | 137 | {"mid", "audio/midi"}, 138 | {"midi", "audio/midi"}, 139 | {"kar", "audio/midi"}, 140 | {"mp3", "audio/mpeg"}, 141 | {"ogg", "audio/ogg"}, 142 | {"m4a", "audio/x-m4a"}, 143 | {"ra", "audio/x-realaudio"}, 144 | 145 | {"3gpp", "video/3gpp"}, 146 | {"3gp", "video/3gpp"}, 147 | {"ts", "video/mp2t"}, 148 | {"mp4", "video/mp4"}, 149 | {"mpeg", "video/mpeg"}, 150 | {"mpg", "video/mpeg"}, 151 | {"mov", "video/quicktime"}, 152 | {"webm", "video/webm"}, 153 | {"flv", "video/x-flv"}, 154 | {"m4v", "video/x-m4v"}, 155 | {"mng", "video/x-mng"}, 156 | {"asx", "video/x-ms-asf"}, 157 | {"asf", "video/x-ms-asf"}, 158 | {"wmv", "video/x-ms-wmv"}, 159 | {"avi", "video/x-msvideo"}, 160 | 161 | }; 162 | 163 | simpleHttpFileServerConnFdStateMach& SimpleHttpFileServerHandler::stateFor(int fd) { 164 | std::lock_guard lk(mu_); 165 | simpleHttpFileServerConnFdStateMach* raw; 166 | auto it = clientfds_.find(fd); 167 | if(it == clientfds_.end()) { 168 | auto xx = std::make_unique(fd); 169 | raw = xx.get(); 170 | clientfds_.emplace(fd, std::move(xx)); 171 | // clientfds_.insert({fd, std::move(xx)}); 172 | // clientfds_[fd] = xx; 173 | // *x = xx; 174 | LOG(INFO, "Adding Connection Socket fd: %d", fd); 175 | } else { 176 | raw = it->second.get(); 177 | } 178 | return *raw; 179 | // auto itb = clientfds_.insert({fd, nullptr}); 180 | // // auto itb = clientfds_.emplace(fd, nullptr); 181 | // if(!itb.second) { 182 | // LOG(INFO, "Adding Connection Socket fd: %d", fd); 183 | // itb.first->second = new simpleHttpFileServerConnFdStateMach(fd); 184 | // } 185 | // *x = itb.first->second; 186 | } 187 | 188 | void SimpleHttpFileServerHandler::unregisterFd(int fd, std::string& err) { 189 | std::lock_guard lk(mu_); 190 | auto it = clientfds_.find(fd); 191 | if(it != clientfds_.end()) { 192 | LOG(INFO, "Removing socket fd: %d", fd); 193 | clientfds_.erase(it); 194 | } 195 | } 196 | 197 | void SimpleHttpFileServerHandler::handleFd(int fd, std::string& err) { 198 | // simpleHttpFileServerConnFdStateMach* h; 199 | auto& h = stateFor(fd); 200 | // h is either waiting, reading, processing, writing 201 | using namespace ugorji::conn; 202 | 203 | std::lock_guard lk(h.mu_); 204 | switch(h.state_) { 205 | case ugorji::conn::CONN_READY: 206 | h.reinit(); 207 | h.state_ = ugorji::conn::CONN_READING; 208 | readFd(h, err); 209 | break; 210 | case ugorji::conn::CONN_READING: 211 | readFd(h, err); 212 | break; 213 | case ugorji::conn::CONN_PROCESSING: 214 | processFd(h, err); 215 | break; 216 | case ugorji::conn::CONN_WRITING: 217 | writeFd(h, err); 218 | } 219 | } 220 | 221 | 222 | void SimpleHttpFileServerHandler::readFd(simpleHttpFileServerConnFdStateMach& h, std::string& err) { 223 | if(h.state_ == ugorji::conn::CONN_READING) { 224 | int n2 = CONN_BUF_INCR; 225 | while(true) { 226 | // always read up to the full expected # bytes, or half if you read less last time. 227 | // this ensures that we don't expand the array unnecessarily. 228 | n2 = (n2 < CONN_BUF_INCR/2) ? CONN_BUF_INCR/2 : CONN_BUF_INCR; 229 | ::slice_bytes_expand(&h.in_, n2); 230 | n2 = ::read(h.fd_, &h.in_.bytes.v[h.in_.bytes.len], n2); 231 | LOG(TRACE, " read %d bytes, into alloc'ed cap: %d", n2, h.in_.cap); 232 | if(n2 > 0) { // TODO what if n2 == 0? 233 | h.in_.bytes.len += n2; 234 | continue; 235 | } 236 | if(n2 == 0) return; // EOF 237 | if(errno == EINTR) continue; 238 | if(errno == EAGAIN || errno == EWOULDBLOCK) { 239 | // check if a http end sequence, and if so, continue to processing, else return 240 | auto len=h.in_.bytes.len; 241 | if(len > 4 && memcmp(&h.in_.bytes.v[len-4], "\r\n\r\n", 4) == 0) { 242 | // ... && h.in_.bytes.v[len-3] == '\n' && h.in_.bytes.v[len-2] == '\r' && ... 243 | break; 244 | } 245 | return; 246 | } 247 | snprintf(errbuf_, 128, "read returned %d, with errno: %s", n2, ugorji::conn::errnoStr().data()); 248 | err = errbuf_; 249 | h.reinit(); 250 | return; 251 | } 252 | h.state_ = ugorji::conn::CONN_PROCESSING; 253 | h.in_cursor_ = 0; 254 | processFd(h, err); 255 | } 256 | } 257 | 258 | void SimpleHttpFileServerHandler::processFd(simpleHttpFileServerConnFdStateMach& h, std::string& err) { 259 | // parse the request path from the headers, transform it, and handle sending the file 260 | // first line should look like: 261 | // GET /pub/WWW/TheProject.html HTTP/1.1 262 | // HEAD /pub/WWW/TheProject.html HTTP/1.1 263 | // 264 | // extract the path, transform it to a path name using simple string manipulation 265 | if(h.state_ == ugorji::conn::CONN_PROCESSING) { 266 | auto in = h.in_.bytes; 267 | size_t m1(h.in_cursor_), m2(0); 268 | if(m1 > in.len-2) { 269 | LOG(TRACE, " done pipeline processing: in_cursor: %d, for bytes read: %d", m1, in.len); 270 | h.reinit(); 271 | return; 272 | } 273 | size_t p1(0), p2(0), h1(0), h2(0); 274 | size_t x1(0), x2(0), x3(0), x4(0); 275 | char st(0); 276 | 277 | LOG(TRACE, " PROCESSING ... ", 0); 278 | size_t i = m1; 279 | // char c = 0; 280 | for(; i < in.len; i++) { 281 | char c = in.v[i]; 282 | if(c == '\n') { 283 | ++i; 284 | break; 285 | } 286 | switch(st) { 287 | case 0: 288 | if(i > 0 && c == ' ') { 289 | m2 = i; 290 | st = 1; 291 | } 292 | break; 293 | case 1: 294 | if(p1 == 0) { 295 | if(c != ' ') p1 = i; 296 | } else if(c == ' ') { 297 | if(p2 == 0) p2 = i; 298 | st = 2; 299 | } else if(c == '?' || c == '#') { 300 | p2 = i; 301 | } 302 | break; 303 | case 2: 304 | if(h1 == 0) { 305 | if(c != ' ') h1 = i; 306 | } else if(c == ' ' || c == '\r' ) { 307 | h2 = i; 308 | st = 3; 309 | } 310 | break; 311 | case 3: 312 | break; 313 | } 314 | } 315 | 316 | LOG(TRACE, " m1: %d, m2: %d, p1: %d, p2: %d, h1: %d, h2: %d", m1, m2, p1, p2, h1, h2); 317 | 318 | if(m2 <= m1 || p2 <= p1 || h2 <= h1) { 319 | err = "unexpected error parsing request"; 320 | h.reinit(); 321 | return; 322 | } 323 | 324 | if(h2 > m1) LOG(TRACE, " request line: %s", std::string(&in.v[m1], h2-m1).data()); 325 | 326 | LOG(TRACE, " looking for headers", 0); 327 | x1 = i; 328 | x2 = 0; 329 | x3 = 0; 330 | x4 = 0; 331 | for(; i < in.len; i++) { 332 | char c = in.v[i]; 333 | if(c == '\n') { 334 | // parse the header and store it 335 | LOG(TRACE, " parsing header line: %s", std::string(&in.v[x1], i-x1).data()); 336 | if(x2-x1 == 4 && strncasecmp(&in.v[x1], "Host", 4) == 0) { 337 | h.host_ = slice_bytes_t{&in.v[x3], x4-x3}; 338 | LOG(TRACE, " found host: %s", std::string(h.host_.v, h.host_.len).data()); 339 | } else if(x2-x1 == 17 && strncasecmp(&in.v[x1], "If-Modified-Since", 17) == 0) { 340 | h.if_mod_since_ = slice_bytes_t{&in.v[x3], x4-x3}; 341 | } else if(x2-x1 == 19 && strncasecmp(&in.v[x1], "If-Unmodified-Since", 19) == 0) { 342 | h.if_unmod_since_ = slice_bytes_t{&in.v[x3], x4-x3}; 343 | } 344 | ++i; 345 | x1 = i; 346 | x2 = 0; 347 | x3 = 0; 348 | x4 = 0; 349 | if(i < in.len-1 && in.v[i] == '\r' && in.v[i+1] == '\n') { 350 | i += 2; 351 | break; 352 | } 353 | continue; 354 | } 355 | if(c == ':') { 356 | if(x2 == 0) x2 = i; 357 | } else if(x2 > 0) { 358 | if(c == '\r') { 359 | x4 = i; 360 | } else if(x3 == 0 && c != ' ') { 361 | x3 = i; 362 | } 363 | } 364 | } 365 | 366 | if(h.host_.len > 0) LOG(TRACE, " header: Host: %s", bytes2cstr(h.host_).data()); 367 | if(h.if_mod_since_.len > 0) LOG(TRACE, " header: If-Modified-Since: %s", bytes2cstr(h.if_mod_since_).data()); 368 | if(h.if_unmod_since_.len > 0) LOG(TRACE, " header: If-Unmodified-Since: %s", bytes2cstr(h.if_unmod_since_).data()); 369 | 370 | std::string httpErrBody = "ERROR SERVING FILE\n"; 371 | std::string xPoweredByHdrLine = 372 | "X-Powered-By: simplehttpfileserver/ugorji\r\n" 373 | "Server: simplehttpfileserver/ugorji"; 374 | 375 | bool pathOK(true), errAdded(false); 376 | if(h.host_.len == 0) { 377 | h.append("HTTP/1.1 400 Bad Request").append("\r\n"); 378 | httpErrBody = "No Host Header\n"; 379 | 380 | pathOK = false; 381 | errAdded = false; 382 | } 383 | 384 | if(m2-m1 == 3 && memcmp(&in.v[m1], "GET", 3) == 0) h.resp_head_only_ = false; 385 | else if(m2-m1 == 4 && memcmp(&in.v[m1], "HEAD", 4) == 0) h.resp_head_only_ = true; 386 | else { 387 | h.append("HTTP/1.1 501 Not Implemented").append("\r\n"); 388 | httpErrBody = std::string(&in.v[m1], m2-m1) + " Method not implemented\n"; 389 | errAdded = true; 390 | pathOK = false; 391 | } 392 | 393 | LOG(TRACE, " before checking path: pathOK = %d", pathOK); 394 | 395 | if(pathOK) { 396 | if(p1 > 0 && p2 > p1 && in.v[p1] == '/') { 397 | h.path_ = std::string_view(&in.v[p1], p2-p1); 398 | LOG(TRACE, " found path: %s", view2cstr(h.path_).data()); 399 | } else pathOK = false; 400 | } 401 | 402 | LOG(TRACE, " before checking http version: pathOK = %d", pathOK); 403 | if(pathOK) { 404 | if(h1 > 0 && h2 > h1 && memcmp(&in.v[h1], "HTTP/1.1", h2-h1) == 0) ; 405 | else pathOK = false; 406 | } 407 | 408 | h.in_cursor_ = i; 409 | 410 | // std::string_view vv(in.v, in.len); 411 | // int a, b, c, d; 412 | // a = vv.find("GET"); 413 | // if(a == 0) { 414 | // b = vv.find(" /", a, 2); 415 | // if(b != std::string_view::npos) { 416 | // b++; 417 | // c = vv.find(' ', b); 418 | // if(c != std::string_view::npos) { 419 | // d = vv.find("HTTP/1.1", c, 8); 420 | // if(d != std::string_view::npos) { 421 | // h.path_ = std::string(vv, b, c-b); 422 | // LOG(TRACE, " found path: %s", view2cstr(h.path_).data()); 423 | // } 424 | // } 425 | // } 426 | // } 427 | // if(h.path_.size() == 0) { 428 | // pathOK = false; 429 | // } 430 | 431 | // parse headers 432 | // e.g. Host: www.ugorji.net\r\n 433 | // 1 2 3 4 434 | 435 | LOG(TRACE, " before checking deny: pathOK = %d", pathOK); 436 | if(pathOK) { 437 | for(auto it = deny_starts_with_.begin(); it != deny_starts_with_.end(); it++) { 438 | if(h.path_.rfind(*it, 0) == 0) { 439 | pathOK = false; 440 | break; 441 | } 442 | } 443 | } 444 | if(pathOK) { 445 | for(auto it = deny_ends_with_.begin(); it != deny_ends_with_.end(); it++) { 446 | auto vv = *it; 447 | auto cmp = h.path_.size() - vv.size(); 448 | if(cmp > 0 && h.path_.find(vv, cmp) == 0) { 449 | pathOK = false; 450 | break; 451 | } 452 | } 453 | } 454 | 455 | std::string filepath(""); 456 | 457 | LOG(TRACE, " before checking mappings: pathOK = %d", pathOK); 458 | if(pathOK) { 459 | pathOK = false; 460 | auto host = std::string(h.host_.v, h.host_.len); 461 | LOG(TRACE, " checking mappings for host: '%s'", host.data()); 462 | auto iter = path_mappings_.find(host); 463 | if(iter != path_mappings_.end()) { 464 | LOG(TRACE, " found mappings for host: %s", host.data()); 465 | auto m3 = *(iter->second); 466 | for(auto it = m3.begin(); it != m3.end(); it++) { 467 | LOG(TRACE, " checking path: %s against mapping: %s", view2cstr(h.path_).data(), it->first.data()); 468 | if(h.path_.rfind(it->first, 0) == 0) { 469 | pathOK=true; 470 | filepath = h.path_; 471 | filepath.replace(0, it->first.size(), it->second, 0, it->second.size()); 472 | LOG(TRACE, " mapped path: %s to filepath: %s", view2cstr(h.path_).data(), filepath.data()); 473 | break; 474 | } 475 | } 476 | } 477 | } 478 | 479 | auto ttnow = std::time(nullptr); 480 | bool useTnow(false); 481 | char tnow[32]; 482 | if(std::strftime(tnow, sizeof(tnow), "%a, %e %b %Y %H:%M:%S GMT", std::gmtime(&ttnow))) useTnow = true; 483 | 484 | if(pathOK) { 485 | h.path_fd_ = open(filepath.data(), O_RDONLY); 486 | if(h.path_fd_ == -1) { 487 | if(errno == EACCES) { 488 | h.append("HTTP/1.1 403 Forbidden").append("\r\n"); 489 | httpErrBody = "Wrong permissions to access file\n"; 490 | } else { 491 | h.append("HTTP/1.1 400 Bad Request").append("\r\n"); 492 | httpErrBody = "Cannot Open File\n"; 493 | } 494 | pathOK = false; 495 | // errAdded = true; 496 | } else { 497 | struct stat statv; 498 | if(fstat(h.path_fd_, &statv) == -1) { 499 | h.append("HTTP/1.1 404 Not Found").append("\r\n"); 500 | httpErrBody = "Missing File\n"; 501 | // errAdded = true; 502 | pathOK = false; 503 | } else if(!S_ISREG(statv.st_mode)) { 504 | h.append("HTTP/1.1 404 Not Found").append("\r\n"); 505 | httpErrBody = "Not a regular file\n"; 506 | // errAdded = true; 507 | pathOK = false; 508 | } else { 509 | // check if_modified_since and if_unmodified_since, and 510 | // appropriately set 304 or 412 respectively. 511 | ::time_t ifModSince(0), ifUnmodSince(0); 512 | char tbuf[32] = {}; 513 | struct ::tm tm = {}; 514 | if(h.if_mod_since_.len > 0) { 515 | // memset(&tm, 0, sizeof(tm)); 516 | ::memmove(tbuf, h.if_mod_since_.v, h.if_mod_since_.len); 517 | ::strptime(tbuf, "%a, %e %b %Y %H:%M:%S GMT", &tm); 518 | ifModSince = ::timegm(&tm); 519 | LOG(TRACE, " checking if-mod-since: %d >= file-last-mod: %d, : %d", 520 | ifModSince, statv.st_mtime, ifModSince >= statv.st_mtime); 521 | if(ifModSince >= statv.st_mtime) { 522 | h.append("HTTP/1.1 304 Not Modified").append("\r\n"); 523 | httpErrBody = "Not Modified since\n"; 524 | // errAdded = true; 525 | pathOK = false; 526 | } 527 | } else if(h.if_unmod_since_.len > 0) { 528 | // memset(&tm, 0, sizeof(tm)); 529 | ::memmove(tbuf, h.if_unmod_since_.v, h.if_unmod_since_.len); 530 | ::strptime(tbuf, "%a, %e %b %Y %H:%M:%S GMT", &tm); 531 | ifUnmodSince = ::timegm(&tm); 532 | LOG(TRACE, " checking if-unmod-since: %d < file-last-mod: %d, : %d", 533 | ifUnmodSince, statv.st_mtime, ifUnmodSince < statv.st_mtime); 534 | if(ifUnmodSince < statv.st_mtime) { 535 | h.append("HTTP/1.1 412 Precondition Failed").append("\r\n"); 536 | httpErrBody = "Precondition Failed\n"; 537 | // errAdded = true; 538 | pathOK = false; 539 | } 540 | } 541 | if(pathOK) { 542 | h.resp_ok_ = true; 543 | h.resp_size_ = statv.st_size; 544 | h.resp_mod_time_ = statv.st_mtime; 545 | h.append("HTTP/1.1 200 OK").append("\r\n"); 546 | std::string mimeType = "application/octet-stream"; 547 | 548 | size_t lastdot = h.path_.rfind('.'); 549 | if(lastdot != std::string_view::npos) { 550 | auto extension = h.path_.substr(lastdot+1); 551 | auto extlen = extension.size(); 552 | if(extlen < 5) { 553 | char nn[5] = {}; 554 | // char c(0); 555 | bool upperCaseFound(false); 556 | for(size_t j = 0; j < extlen; j++) { 557 | char k = extension[j]; 558 | if(k >= 'A' && k <= 'Z') { 559 | upperCaseFound = true; 560 | nn[j] = k + 'a' - 'A'; 561 | } else nn[j] = k; 562 | } 563 | if(upperCaseFound) extension = std::string_view(nn, extlen); 564 | } 565 | auto it = MIME_TYPES.find(extension); 566 | if(it != MIME_TYPES.end()) mimeType = it->second; 567 | LOG(TRACE, " extension: %s, mimeType: %s", view2cstr(extension).data(), mimeType.data()); 568 | } 569 | if(std::strftime(tbuf, sizeof(tbuf), "%a, %e %b %Y %H:%M:%S GMT", std::gmtime(&h.resp_mod_time_))) { 570 | h.append("Last-Modified: ").append(tbuf).append("\r\n"); 571 | } 572 | h.append("Content-Type: ").append(mimeType).append("\r\n") 573 | .append("Connection: keep-alive").append("\r\n") 574 | .append("Content-Length: ").append(std::to_string(h.resp_size_)).append("\r\n"); 575 | if(useTnow) h.append("Date: ").append(tnow).append("\r\n"); 576 | h.append(xPoweredByHdrLine).append("\r\n") 577 | .append("\r\n"); 578 | } 579 | } 580 | } 581 | } else if(!errAdded) { 582 | h.append("HTTP/1.1 400 Bad Request").append("\r\n"); 583 | httpErrBody = "Invalid Path\n"; 584 | //errAdded = true; 585 | } 586 | 587 | if(!h.resp_ok_) { 588 | h.resp_size_ = httpErrBody.size(); 589 | h.append("Content-Type: text/plain; charset=UTF-8").append("\r\n") 590 | .append("Connection: keep-alive").append("\r\n") 591 | .append("Content-Length: ").append(std::to_string(h.resp_size_)).append("\r\n"); 592 | if(useTnow) h.append("Date: ").append(tnow).append("\r\n"); 593 | h.append(xPoweredByHdrLine).append("\r\n") 594 | .append("\r\n"); 595 | h.append(httpErrBody); 596 | } 597 | h.state_ = ugorji::conn::CONN_WRITING; 598 | writeFd(h, err); 599 | } 600 | } 601 | 602 | void SimpleHttpFileServerHandler::writeFd(simpleHttpFileServerConnFdStateMach& h, std::string& err) { 603 | if(h.state_ == ugorji::conn::CONN_WRITING) { 604 | LOG(TRACE, ": writing output to fd: %d", h.fd_); 605 | LOG(TRACE, ": writing fd: %d, path: %s", h.fd_, view2cstr(h.path_).data()); 606 | int z = h.resp_.bytes.len; 607 | while(h.out_cursor_ < z) { 608 | int n2 = ::write(h.fd_, &h.resp_.bytes.v[h.out_cursor_], z-h.out_cursor_); 609 | if(n2 < 0) { 610 | if(errno == EINTR) continue; 611 | if(errno == EAGAIN || errno == EWOULDBLOCK) return; 612 | snprintf(errbuf_, 128, "write returned %d, with errno: %s", n2, ugorji::conn::errnoStr().data()); 613 | err = errbuf_; 614 | h.reinit(); 615 | return; 616 | } 617 | h.out_cursor_ += n2; 618 | } 619 | 620 | if(h.resp_ok_ && !h.resp_head_only_) { 621 | // sendfile(h.fd_, h.path_fd_, &off, h.resp_size_); // blocking call 622 | // sendfile in chunks of 128K 623 | while(h.sendfile_offset_ < h.resp_size_) { 624 | auto n2 = sendfile(h.fd_, h.path_fd_, &h.sendfile_offset_, h.resp_size_ - h.sendfile_offset_); 625 | if(n2 < 0) { 626 | if(errno == EINTR) continue; 627 | if(errno == EAGAIN || errno == EWOULDBLOCK) return; 628 | snprintf(errbuf_, 128, "sendfile returned %ld, with errno: %s", n2, ugorji::conn::errnoStr().data()); 629 | // err = std::string(errbuf_); 630 | err = errbuf_; 631 | h.reinit(); 632 | return; 633 | } 634 | } 635 | } 636 | 637 | if(h.path_fd_ > 2) close(h.path_fd_); // file dscriptors 0, 1, 2 are stdin/out/err always 638 | h.writeDone(); 639 | h.state_ = ugorji::conn::CONN_PROCESSING; 640 | processFd(h, err); 641 | } 642 | } 643 | 644 | --------------------------------------------------------------------------------