/** httpserver HTTPServer.cpp Copyright 2011-2025 Ramsey Kant Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ #include "HTTPServer.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef __linux__ #include // libkqueue Linux - only works if libkqueue is compiled from Github sources #else #include // kqueue BSD / OS X #endif /** * Server Constructor * Initialize state and server variables * * @param vhost_aliases List of hostnames the HTTP server will respond to * @param port Port the vhost listens on * @param diskpath Path to the folder the vhost serves up * @param drop_uid UID to setuid to after bind(). Ignored if 0 * @param drop_gid GID to setgid to after bind(). Ignored if 0 */ HTTPServer::HTTPServer(std::vector const& vhost_aliases, int32_t port, std::string const& diskpath, int32_t drop_uid, int32_t drop_gid) : listenPort(port), dropUid(drop_uid), dropGid(drop_gid) { std::print("Port: {}\n", port); std::print("Disk path: {}\n", diskpath); // Create a resource host serving the base path ./htdocs on disk auto resHost = std::make_shared(diskpath); hostList.push_back(resHost); // Always serve up localhost/127.0.0.1 (which is why we only added one ResourceHost to hostList above) vhosts.try_emplace(std::format("localhost:{}", listenPort), resHost); vhosts.try_emplace(std::format("127.0.0.1:{}", listenPort), resHost); // Setup the resource host serving htdocs to provide for the vhost aliases for (auto const& vh : vhost_aliases) { if (vh.length() >= 122) { std::print("vhost {} too long, skipping!\n", vh); continue; } std::print("vhost: {}\n", vh); vhosts.try_emplace(std::format("{}:{}", vh, listenPort), resHost); } } /** * Server Destructor * Removes all resources created in the constructor */ HTTPServer::~HTTPServer() { hostList.clear(); vhosts.clear(); } /** * Start Server * Initialize the Server Socket by requesting a socket handle, binding, and going into a listening state * * @return True if initialization succeeded. False if otherwise */ bool HTTPServer::start() { canRun = false; // Create a handle for the listening socket, TCP listenSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (listenSocket == INVALID_SOCKET) { std::print("Could not create socket!\n"); return false; } // Allow immediate reuse of the port after restart (prevents "address already in use" on quick restarts) int32_t opt = 1; if (setsockopt(listenSocket, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) != 0) { std::print("Failed to set SO_REUSEADDR\n"); return false; } // Set socket as non-blocking if (fcntl(listenSocket, F_SETFL, O_NONBLOCK) == -1) { std::print("Failed to set listen socket non-blocking\n"); return false; } // Populate the server address structure // modify to support multiple address families (bottom): http://eradman.com/posts/kqueue-tcp.html memset(&serverAddr, 0, sizeof(struct sockaddr_in)); // clear the struct serverAddr.sin_family = AF_INET; // Family: IP protocol serverAddr.sin_port = htons(listenPort); // Set the port (convert from host to netbyte order) serverAddr.sin_addr.s_addr = INADDR_ANY; // Let OS intelligently select the server's host address // Bind: Assign the address to the socket if (bind(listenSocket, (struct sockaddr*)&serverAddr, sizeof(serverAddr)) != 0) { std::print("Failed to bind to the address!\n"); return false; } // Optionally drop uid/gid if specified if (dropUid > 0 && dropGid > 0) { if (setgid(dropGid) != 0) { std::print("setgid to {} failed!\n", dropGid); return false; } if (setuid(dropUid) != 0) { std::print("setuid to {} failed!\n", dropUid); return false; } std::print("Successfully dropped uid to {} and gid to {}\n", dropUid, dropGid); } // Listen: Put the socket in a listening state, ready to accept connections // Accept a backlog of the OS Maximum connections in the queue if (listen(listenSocket, SOMAXCONN) != 0) { std::print("Failed to put the socket in a listening state\n"); return false; } // Setup kqueue kqfd = kqueue(); if (kqfd == -1) { std::print("Could not create the kernel event queue!\n"); return false; } // Have kqueue watch the listen socket updateEvent(listenSocket, EVFILT_READ, EV_ADD, 0, 0, NULL); canRun = true; std::print("Server ready. Listening on port {}...\n", listenPort); return true; } /** * Stop * Disconnect all clients and cleanup all server resources created in start() */ void HTTPServer::stop() { canRun = false; if (listenSocket != INVALID_SOCKET) { // Close all open connections and delete Client's from memory for (auto& [clfd, cl] : clientMap) disconnectClient(cl, false); // Clear the map clientMap.clear(); // Remove listening socket from kqueue updateEvent(listenSocket, EVFILT_READ, EV_DELETE, 0, 0, NULL); // Shudown the listening socket and release it to the OS shutdown(listenSocket, SHUT_RDWR); close(listenSocket); listenSocket = INVALID_SOCKET; } if (kqfd != -1) { close(kqfd); kqfd = -1; } std::print("Server shutdown!\n"); } /** * Update Event * Update the kqueue by creating the appropriate kevent structure * See kqueue documentation for parameter descriptions */ void HTTPServer::updateEvent(int32_t ident, int16_t filter, uint16_t flags, uint32_t fflags, int32_t data, void* udata) { struct kevent kev; EV_SET(&kev, ident, filter, flags, fflags, data, udata); kevent(kqfd, &kev, 1, NULL, 0, NULL); } /** * Server Process * Main server processing function that checks for any new connections or data to read on * the listening socket */ void HTTPServer::process() { int32_t nev = 0; // Number of changed events returned by kevent while (canRun) { // Get a list of changed socket descriptors with a read event triggered in evList // Timeout set in the header nev = kevent(kqfd, NULL, 0, evList.data(), QUEUE_SIZE, &kqTimeout); if (nev <= 0) continue; // Loop through only the sockets that have changed in the evList array for (int32_t i = 0; i < nev; i++) { // A client is waiting to connect if (evList[i].ident == static_cast(listenSocket)) { acceptConnection(); continue; } // Client descriptor has triggered an event auto cl = getClient(evList[i].ident); // ident contains the clients socket descriptor if (cl == nullptr) { std::print("Could not find client\n"); // Remove socket events from kqueue updateEvent(evList[i].ident, EVFILT_READ, EV_DELETE, 0, 0, NULL); updateEvent(evList[i].ident, EVFILT_WRITE, EV_DELETE, 0, 0, NULL); // Close the socket descriptor close(evList[i].ident); continue; } // Client wants to disconnect if (evList[i].flags & EV_EOF) { disconnectClient(cl, true); continue; } if (evList[i].filter == EVFILT_READ) { // std::print("read filter {} bytes available\n", evList[i].data); // Read and process any pending data on the wire readClient(cl, evList[i].data); // data contains the number of bytes waiting to be read // Have kqueue disable tracking of READ events and enable tracking of WRITE events updateEvent(evList[i].ident, EVFILT_READ, EV_DISABLE, 0, 0, NULL); updateEvent(evList[i].ident, EVFILT_WRITE, EV_ENABLE, 0, 0, NULL); } else if (evList[i].filter == EVFILT_WRITE) { // std::print("write filter with {} bytes available\n", evList[i].data); // Write any pending data to the client - writeClient returns true if there is additional data to send in the client queue if (!writeClient(cl, evList[i].data)) { // data contains number of bytes that can be written // std::print("switch back to read filter\n"); // If theres nothing more to send, Have kqueue disable tracking of WRITE events and enable tracking of READ events updateEvent(evList[i].ident, EVFILT_READ, EV_ENABLE, 0, 0, NULL); updateEvent(evList[i].ident, EVFILT_WRITE, EV_DISABLE, 0, 0, NULL); } } } // Event loop } // canRun } /** * Accept Connection * When a new connection is detected in runServer() this function is called. This attempts to accept the pending * connection, instance a Client object, and add to the client Map */ void HTTPServer::acceptConnection() { // Setup new client with prelim address info sockaddr_in clientAddr; int32_t clientAddrLen = sizeof(clientAddr); int32_t clfd = INVALID_SOCKET; // Accept the pending connection and retrive the client descriptor clfd = accept(listenSocket, (sockaddr*)&clientAddr, (socklen_t*)&clientAddrLen); if (clfd == INVALID_SOCKET) return; // Reject the connection if the client limit has been reached to prevent file descriptor exhaustion if (clientMap.size() >= MAX_CLIENTS) { close(clfd); return; } // Set socket as non-blocking; close and reject the connection if this fails if (fcntl(clfd, F_SETFL, O_NONBLOCK) == -1) { std::print("Failed to set client socket non-blocking, rejecting connection\n"); close(clfd); return; } // Add kqueue event to track the new client socket for READ and WRITE events updateEvent(clfd, EVFILT_READ, EV_ADD | EV_ENABLE, 0, 0, NULL); updateEvent(clfd, EVFILT_WRITE, EV_ADD | EV_DISABLE, 0, 0, NULL); // Disabled initially // Add the client object to the client map auto cl = std::make_unique(clfd, clientAddr); std::print("[{}] connected\n", cl->getClientIP()); clientMap.try_emplace(clfd, std::move(cl)); } /** * Get Client * Lookup client based on the socket descriptor number in the clientMap * * @param clfd Client socket descriptor * @return Pointer to Client object if found. NULL otherwise */ std::shared_ptr HTTPServer::getClient(int32_t clfd) { auto it = clientMap.find(clfd); // Client wasn't found if (it == clientMap.end()) return nullptr; // Return a pointer to the client object return it->second; } /** * Disconnect Client * Close the client's socket descriptor and release it from the FD map, client map, and memory * * @param cl Pointer to Client object * @param mapErase When true, remove the client from the client map. Needed if operations on the * client map are being performed and we don't want to remove the map entry right away */ void HTTPServer::disconnectClient(std::shared_ptr cl, bool mapErase) { if (cl == nullptr) return; std::print("[{}] disconnected\n", cl->getClientIP()); // Remove socket events from kqueue updateEvent(cl->getSocket(), EVFILT_READ, EV_DELETE, 0, 0, NULL); updateEvent(cl->getSocket(), EVFILT_WRITE, EV_DELETE, 0, 0, NULL); // Close the socket descriptor close(cl->getSocket()); // Remove the client from the clientMap if (mapErase) clientMap.erase(cl->getSocket()); } /** * Read Client * Recieve data from a client that has indicated that it has data waiting. Pass recv'd data to handleRequest() * Also detect any errors in the state of the socket * * @param cl Pointer to Client that sent the data * @param data_len Number of bytes waiting to be read */ void HTTPServer::readClient(std::shared_ptr cl, int32_t data_len) { if (cl == nullptr) return; // If the read filter triggered with 0 bytes of data, client may want to disconnect // Set data_len to the Ethernet max MTU by default constexpr int32_t MAX_READ_SIZE = 8 * 1024 * 1024; // 8 MB per-read cap if (data_len <= 0) data_len = 1400; else if (data_len > MAX_READ_SIZE) data_len = MAX_READ_SIZE; auto pData = std::make_unique(data_len); // Receive data on the wire into pData int32_t flags = 0; ssize_t lenRecv = recv(cl->getSocket(), pData.get(), data_len, flags); // Determine state of the client socket and act on it if (lenRecv == 0) { // Client closed the connection std::print("[{}] has opted to close the connection\n", cl->getClientIP()); disconnectClient(cl, true); } else if (lenRecv < 0) { // Something went wrong with the connection // TODO: check perror() for the specific error message disconnectClient(cl, true); } else { // Data received: Place the data in an HTTPRequest and pass it to handleRequest for processing auto req = std::make_unique(pData.get(), lenRecv); handleRequest(cl, std::move(req)); } } /** * Write Client * Client has indicated it is read for writing. Write avail_bytes number of bytes to the socket if the send queue has an item * * @param cl Pointer to Client that sent the data * @param avail_bytes Number of bytes available for writing in the send buffer */ bool HTTPServer::writeClient(std::shared_ptr cl, int32_t avail_bytes) { if (cl == nullptr) return false; int32_t actual_sent = 0; // Actual number of bytes sent as returned by send() int32_t attempt_sent = 0; // Bytes that we're attempting to send now // The amount of available bytes to write, reported by the OS, cant really be trusted... if (avail_bytes > 1400) { // If the available amount of data is greater than the Ethernet MTU, cap it avail_bytes = 1400; } else if (avail_bytes == 0) { // Sometimes OS reports 0 when its possible to send data - attempt to trickle data // OS will eventually increase avail_bytes avail_bytes = 64; } auto item = cl->nextInSendQueue(); if (item == nullptr) return false; const uint8_t* const pData = item->getRawDataPointer(); // Size of data left to send for the item int32_t remaining = item->getSize() - item->getOffset(); bool disconnect = item->getDisconnect(); if (avail_bytes >= remaining) { // Send buffer is bigger than we need, rest of item can be sent attempt_sent = remaining; } else { // Send buffer is smaller than we need, send the amount thats available attempt_sent = avail_bytes; } // Send the data and increment the offset by the actual amount sent actual_sent = send(cl->getSocket(), pData + (item->getOffset()), attempt_sent, 0); if (actual_sent >= 0) item->setOffset(item->getOffset() + actual_sent); else disconnect = true; // std::print("[{}] was sent {} bytes\n", cl->getClientIP(), actual_sent); // SendQueueItem isnt needed anymore. Dequeue and delete if (item->getOffset() >= item->getSize()) cl->dequeueFromSendQueue(); if (disconnect) { disconnectClient(cl, true); return false; } return true; } /** * Handle Request * Process an incoming request from a Client. Send request off to appropriate handler function * that corresponds to an HTTP operation (GET, HEAD etc) :) * * @param cl Client object where request originated from * @param req HTTPRequest object filled with raw packet data */ void HTTPServer::handleRequest(std::shared_ptr cl, std::shared_ptr req) { // Parse the request // If there's an error, report it and send a server error in response if (!req->parse()) { std::print("[{}] There was an error processing the request of type: {}\n", cl->getClientIP(), req->methodIntToStr(req->getMethod())); std::print("{}\n", req->getParseError()); sendStatusResponse(cl, Status(BAD_REQUEST)); return; } std::print("[{}] {} {}\n", cl->getClientIP(), req->methodIntToStr(req->getMethod()), req->getRequestUri()); /*std::print("Headers:\n"); for (uint32_t i = 0; i < req->getNumHeaders(); i++) { std::print("\t{}\n", req->getHeaderStr(i)); } std::print("\n");*/ // Send the request to the correct handler function switch (req->getMethod()) { case Method(HEAD): case Method(GET): handleGet(cl, req); break; case Method(OPTIONS): handleOptions(cl, req); break; case Method(TRACE): handleTrace(cl, req); break; default: std::print("[{}] Could not handle or determine request of type {}\n", cl->getClientIP(), req->methodIntToStr(req->getMethod())); sendStatusResponse(cl, Status(NOT_IMPLEMENTED)); break; } } /** * Handle Get or Head * Process a GET or HEAD request to provide the client with an appropriate response * * @param cl Client requesting the resource * @param req State of the request */ void HTTPServer::handleGet(std::shared_ptr cl, const std::shared_ptr req) { auto resHost = this->getResourceHostForRequest(req); // ResourceHost couldnt be determined or the Host specified by the client was invalid if (resHost == nullptr) { sendStatusResponse(cl, Status(BAD_REQUEST), "Invalid/No Host specified"); return; } // Check if the requested resource exists auto uri = req->getRequestUri(); auto resource = resHost->getResource(uri); if (resource != nullptr) { // Exists std::print("[{}] Sending file: {}\n", cl->getClientIP(), uri); auto resp = std::make_unique(); resp->setStatus(Status(OK)); resp->addHeader("Content-Type", resource->getMimeType()); resp->addHeader("Content-Length", resource->getSize()); // Only send a message body if it's a GET request. Never send a body for HEAD if (req->getMethod() == Method(GET)) resp->setData(resource->getData(), resource->getSize()); bool dc = false; // HTTP/1.0 should close the connection by default if (req->getVersion().compare(HTTP_VERSION_10) == 0) dc = true; // If Connection: close is specified, the connection should be terminated after the request is serviced if (auto con_val = req->getHeaderValue("Connection"); con_val.compare("close") == 0) dc = true; sendResponse(cl, std::move(resp), dc); } else { // Not found std::print("[{}] File not found: {}\n", cl->getClientIP(), uri); sendStatusResponse(cl, Status(NOT_FOUND)); } } /** * Handle Options * Process a OPTIONS request * OPTIONS: Return allowed capabilties for the server (*) or a particular resource * * @param cl Client requesting the resource * @param req State of the request */ void HTTPServer::handleOptions(std::shared_ptr cl, [[maybe_unused]] const std::shared_ptr req) { // For now, we'll always return the capabilities of the server instead of figuring it out for each resource std::string allow = "HEAD, GET, OPTIONS, TRACE"; auto resp = std::make_unique(); resp->setStatus(Status(OK)); resp->addHeader("Allow", allow); resp->addHeader("Content-Length", "0"); // Required sendResponse(cl, std::move(resp), true); } /** * Handle Trace * Process a TRACE request * TRACE: send back the request as received by the server verbatim * * @param cl Client requesting the resource * @param req State of the request */ void HTTPServer::handleTrace(std::shared_ptr cl, std::shared_ptr req) { // Get a byte array representation of the request uint32_t len = req->size(); auto buf = std::make_unique(len); req->setReadPos(0); // Set the read position at the beginning since the request has already been read to the end req->getBytes(buf.get(), len); // Send a response with the entire request as the body auto resp = std::make_unique(); resp->setStatus(Status(OK)); resp->addHeader("Content-Type", "message/http"); resp->addHeader("Content-Length", len); resp->setData(buf.get(), len); sendResponse(cl, std::move(resp), true); } /** * Send Status Response * Send a predefined HTTP status code response to the client consisting of * only the status code and required headers, then disconnect the client * * @param cl Client to send the status code to * @param status Status code corresponding to the enum in HTTPMessage.h * @param msg An additional message to append to the body text */ void HTTPServer::sendStatusResponse(std::shared_ptr cl, int32_t status, std::string const& msg) { auto resp = std::make_unique(); resp->setStatus(status); // Body message: Reason string + additional msg std::string body = resp->getReason(); if (msg.length() > 0) body += ": " + msg; uint32_t slen = body.length(); resp->addHeader("Content-Type", "text/plain"); resp->addHeader("Content-Length", slen); resp->setData(reinterpret_cast(body.data()), slen); sendResponse(cl, std::move(resp), true); } /** * Send Response * Send a generic HTTPResponse packet data to a particular Client * * @param cl Client to send data to * @param buf ByteBuffer containing data to be sent * @param disconnect Should the server disconnect the client after sending (Optional, default = false) */ void HTTPServer::sendResponse(std::shared_ptr cl, std::unique_ptr resp, bool disconnect) { // Server Header resp->addHeader("Server", "httpserver/1.0"); // Timestamp the response with the Date header casted to seconds precision const auto now_utc = std::chrono::system_clock::now(); const auto now_seconds = std::chrono::time_point_cast(now_utc); // Ex: Fri, 31 Dec 1999 23:59:59 GMT resp->addHeader("Date", std::format("{:%a, %d %b %Y %H:%M:%S GMT}", now_seconds)); // Include a Connection: close header if this is the final response sent by the server if (disconnect) resp->addHeader("Connection", "close"); // Get raw data by creating the response (we are responsible for cleaning it up in process()) // Add data to the Client's send queue cl->addToSendQueue(std::make_shared(resp->create(), resp->size(), disconnect)); } /** * Get Resource Host * Retrieve the appropriate ResourceHost instance based on the requested path * * @param req State of the request */ std::shared_ptr HTTPServer::getResourceHostForRequest(const std::shared_ptr req) { // Determine the appropriate vhost std::string host = ""; // Retrieve the host specified in the request (Required for HTTP/1.1 compliance) if (req->getVersion().compare(HTTP_VERSION_11) == 0) { host = req->getHeaderValue("Host"); // All vhosts have the port appended, so need to append it to the host if it doesnt exist if (!host.contains(":")) { host.append(std::format(":{}", listenPort)); } auto it = vhosts.find(host); if (it != vhosts.end()) return it->second; } else { // Temporary: HTTP/1.0 are given the first ResouceHost in the hostList if (!hostList.empty()) return hostList[0]; } return nullptr; }