diff --git a/userspace/http-server/CMakeLists.txt b/userspace/http-server/CMakeLists.txt index 3c5928e1..9a0bd795 100644 --- a/userspace/http-server/CMakeLists.txt +++ b/userspace/http-server/CMakeLists.txt @@ -1,5 +1,6 @@ set(SOURCES main.cpp + HTTPServer.cpp ) add_executable(http-server ${SOURCES}) diff --git a/userspace/http-server/HTTPServer.cpp b/userspace/http-server/HTTPServer.cpp new file mode 100644 index 00000000..26ef685f --- /dev/null +++ b/userspace/http-server/HTTPServer.cpp @@ -0,0 +1,516 @@ +#include "HTTPServer.h" + +#include +#include + +#include +#include +#include +#include +#include +#include + +static BAN::StringView status_to_brief(unsigned); +static BAN::StringView extension_to_mime(BAN::StringView); + +HTTPServer::HTTPServer() = default; + +HTTPServer::~HTTPServer() +{ + if (m_listen_socket != -1) + close(m_listen_socket); +} + +BAN::ErrorOr HTTPServer::initialize(BAN::StringView root, BAN::IPv4Address ip, int port) +{ + { + char path_buffer[PATH_MAX]; + if (root.size() >= PATH_MAX) + return BAN::Error::from_errno(ENAMETOOLONG); + strcpy(path_buffer, root.data()); + + char canonical_buffer[PATH_MAX]; + if (realpath(path_buffer, canonical_buffer) == NULL) + return BAN::Error::from_errno(errno); + + TRY(m_web_root.append(canonical_buffer)); + } + + sockaddr_in addr; + addr.sin_family = AF_INET; + addr.sin_port = htons(port); + addr.sin_addr.s_addr = ip.raw; + + int sock = socket(AF_INET, SOCK_STREAM, 0); + if (sock == -1) + return BAN::Error::from_errno(errno); + BAN::ScopeGuard socket_guard([sock] { close(sock); }); + + if (bind(sock, reinterpret_cast(&addr), sizeof(addr)) == -1) + return BAN::Error::from_errno(errno); + + if (listen(sock, SOMAXCONN) == -1) + return BAN::Error::from_errno(errno); + + m_listen_socket = sock; + + socket_guard.disable(); + return {}; +} + +void HTTPServer::start() +{ + ASSERT(m_listen_socket != -1); + + while (true) + { + fd_set fds; + FD_ZERO(&fds); + + FD_SET(m_listen_socket, &fds); + int max_fd = m_listen_socket; + + for (const auto& [fd, _] : m_client_data) + { + FD_SET(fd, &fds); + max_fd = BAN::Math::max(max_fd, fd); + } + + if (select(max_fd + 1, &fds, nullptr, nullptr, nullptr) == -1) + { + perror("select"); + break; + } + + if (FD_ISSET(m_listen_socket, &fds)) + { + int new_fd = accept(m_listen_socket, nullptr, nullptr); + if (new_fd == -1) + { + perror("accept"); + continue; + } + MUST(m_client_data.emplace(new_fd)); + } + + for (auto& [fd, data] : m_client_data) + { + if (!FD_ISSET(fd, &fds)) + continue; + + char buffer[1024]; + int nrecv = recv(fd, buffer, sizeof(buffer), 0); + if (nrecv < 0) + perror("recv"); + if (nrecv <= 0) + { + close(fd); + m_client_data.remove(fd); + break; + } + + size_t old_size = data.size(); + if (data.resize(old_size + nrecv).is_error()) + { + close(fd); + m_client_data.remove(fd); + break; + } + + memcpy(data.data() + old_size, buffer, nrecv); + + if (!handle_all_requests(fd, data)) + { + close(fd); + m_client_data.remove(fd); + break; + } + } + } +} + +BAN::ErrorOr HTTPServer::get_http_request(BAN::Vector& data_vec) +{ + auto data = BAN::ConstByteSpan(data_vec.span()); + + if (data.size() < 4) + return BAN::Error::from_errno(ENODATA); + size_t len = 0; + for (;; len++) + { + if (len > data.size() - 4) + return BAN::Error::from_errno(ENODATA); + if (!isprint(data[len]) && !isspace(data[len])) + return BAN::Error::from_errno(EINVAL); + if (data[len + 0] != '\r') + continue; + if (data[len + 1] != '\n') + continue; + if (data[len + 2] != '\r') + continue; + if (data[len + 3] != '\n') + continue; + break; + } + + auto header_data = BAN::StringView(reinterpret_cast(data.data()), len + 1); + + auto lines = TRY(header_data.split('\n', false)); + if (lines.empty()) + return BAN::Error::from_errno(EINVAL); + for (auto& line : lines) + { + if (line.empty() || line.back() != '\r') + return BAN::Error::from_errno(EINVAL); + line = line.substring(0, line.size() - 1); + } + + HTTPRequest request; + + { + auto request_line = TRY(lines[0].split(' ')); + if (request_line.size() != 3) + return BAN::Error::from_errno(EINVAL); + request.method = request_line[0]; + request.path = request_line[1]; + request.version = request_line[2]; + } + + size_t content_length = 0; + for (size_t i = 1; i < lines.size(); i++) + { + auto opt_colon = lines[i].find(':'); + if (!opt_colon.has_value()) + return BAN::Error::from_errno(EINVAL); + + auto name = lines[i].substring(0, opt_colon.value()); + + auto value = lines[i].substring(opt_colon.value() + 1); + while (!value.empty() && isspace(value.front())) + value = value.substring(1); + while (!value.empty() && isspace(value.back())) + value = value.substring(0, value.size() - 1); + + TRY(request.headers.emplace_back(name, value)); + + if (name.size() == "Content-Length"_sv.size()) + { + bool is_content_length = true; + for (size_t i = 0; i < name.size() && is_content_length; i++) + if (tolower(name[i]) != tolower("Content-Length"_sv[i])) + is_content_length = false; + if (is_content_length) + content_length = strtoul(value.data(), nullptr, 10); + } + } + + if (data.size() < len + 4 + content_length) + return BAN::Error::from_errno(ENODATA); + + request.body = data.slice(len + 4, content_length); + + { + size_t request_size = len + 4 + content_length; + size_t new_size = data.size() - request_size; + memmove(data_vec.data(), data_vec.data() + request_size, new_size); + MUST(data_vec.resize(new_size)); + } + + return request; +} + +BAN::ErrorOr HTTPServer::send_http_response(int fd, unsigned status, BAN::ConstByteSpan data, BAN::StringView mime) +{ + dprintln("HTTP/1.1 {} {}", status, status_to_brief(status)); + + BAN::String output; + TRY(output.append(MUST(BAN::String::formatted("HTTP/1.1 {} {}\r\n", status, status_to_brief(status))))); + if (!mime.empty()) + TRY(output.append(MUST(BAN::String::formatted("Content-Type: {}\r\n", mime)))); + TRY(output.append(MUST(BAN::String::formatted("Content-Length: {}\r\n", data.size())))); + TRY(output.append("\r\n")); + + size_t total_sent = 0; + while (total_sent < output.size()) + { + ssize_t nsend = send(fd, output.data() + total_sent, output.size() - total_sent, 0); + if (nsend == -1) + return BAN::Error::from_errno(errno); + if (nsend == 0) + return BAN::Error::from_errno(ECONNRESET); + total_sent += nsend; + } + + total_sent = 0; + while (total_sent < data.size()) + { + ssize_t nsend = send(fd, data.data() + total_sent, data.size() - total_sent, 0); + if (nsend == -1) + return BAN::Error::from_errno(errno); + if (nsend == 0) + return BAN::Error::from_errno(ECONNRESET); + total_sent += nsend; + } + + return {}; +} + +BAN::ErrorOr HTTPServer::handle_request(int fd, BAN::Vector& data) +{ + auto request_or_error = get_http_request(data); + if (request_or_error.is_error()) + return request_or_error.release_error(); + auto request = request_or_error.release_value(); + + dprintln("{} {} {}", request.method, request.path, request.version); + + // remove query string + if (auto idx = request.path.find('?'); idx.has_value()) + request.path = request.path.substring(0, idx.value()); + + // illegal path + if (request.path.empty() || request.path.front() != '/') + return 400; + + BAN::StringView path_suffix; + if (request.path.back() == '/') + path_suffix = "index.html"_sv; + else + { + auto file = request.path.substring(request.path.rfind('/').value()); + if (!file.contains('.')) + path_suffix = ".html"_sv; + } + + auto target_path = TRY(BAN::String::formatted("{}{}{}", m_web_root, request.path, path_suffix)); + auto extension = target_path.sv().substring(target_path.sv().rfind('.').value()); + + dprintln("looking for '{}'", target_path); + + char canonical_buffer[PATH_MAX]; + if (realpath(target_path.data(), canonical_buffer) == NULL) + { + switch (errno) + { + case EACCES: + return 403; + case ENAMETOOLONG: + case ENOENT: + case ENOTDIR: + return 404; + case ELOOP: + return 500; + default: + return BAN::Error::from_errno(errno); + } + } + dprintln("validating '{}'", canonical_buffer); + if (strncmp(canonical_buffer, m_web_root.data(), m_web_root.size())) + return BAN::Error::from_errno(403); + + int file_fd = open(canonical_buffer, O_RDONLY); + if (file_fd == -1) + return (errno == EACCES) ? 403 : 404; + BAN::ScopeGuard _([file_fd] { close(file_fd); }); + + struct stat file_st; + if (fstat(file_fd, &file_st) == -1) + return 500; + + BAN::Vector file_data; + if (file_data.resize(file_st.st_size).is_error()) + return 500; + + if (read(file_fd, file_data.data(), file_data.size()) == -1) + return 500; + + TRY(send_http_response(fd, 200, BAN::ConstByteSpan(file_data.span()), extension_to_mime(extension))); + + return 200; +} + +bool HTTPServer::handle_all_requests(int fd, BAN::Vector& data) +{ + while (true) + { + auto result = handle_request(fd, data); + if (result.is_error() && result.error().get_error_code() == ENODATA) + return true; + if (result.is_error()) + return false; + if (result.value() == 200) + continue; + if (send_http_response(fd, result.value(), {}, {}).is_error()) + return false; + } +} + +static BAN::StringView status_to_brief(unsigned status) +{ + static BAN::HashMap status_to_brief; + if (status_to_brief.empty()) + { + MUST(status_to_brief.emplace(100, "Continue"_sv)); + MUST(status_to_brief.emplace(101, "Switching Protocols"_sv)); + MUST(status_to_brief.emplace(102, "Processing"_sv)); + MUST(status_to_brief.emplace(103, "Early Hints"_sv)); + + MUST(status_to_brief.emplace(200, "OK"_sv)); + MUST(status_to_brief.emplace(201, "Created"_sv)); + MUST(status_to_brief.emplace(202, "Accepted"_sv)); + MUST(status_to_brief.emplace(203, "Non-Authoritative Information"_sv)); + MUST(status_to_brief.emplace(204, "No Content"_sv)); + MUST(status_to_brief.emplace(205, "Reset Content"_sv)); + MUST(status_to_brief.emplace(206, "Partial Content"_sv)); + MUST(status_to_brief.emplace(207, "Multi-Status"_sv)); + MUST(status_to_brief.emplace(208, "Already Reported"_sv)); + MUST(status_to_brief.emplace(226, "IM Used"_sv)); + + MUST(status_to_brief.emplace(300, "Multiple Choices"_sv)); + MUST(status_to_brief.emplace(301, "Moved Permanently"_sv)); + MUST(status_to_brief.emplace(302, "Found"_sv)); + MUST(status_to_brief.emplace(303, "See Other"_sv)); + MUST(status_to_brief.emplace(304, "Not Modified"_sv)); + MUST(status_to_brief.emplace(305, "Use Proxy"_sv)); + MUST(status_to_brief.emplace(306, "Switch Proxy"_sv)); + MUST(status_to_brief.emplace(307, "Temporary Redirect"_sv)); + MUST(status_to_brief.emplace(308, "Permanent Redirect"_sv)); + + MUST(status_to_brief.emplace(400, "Bad Request"_sv)); + MUST(status_to_brief.emplace(401, "Unauthorized"_sv)); + MUST(status_to_brief.emplace(402, "Payment Required Experimental"_sv)); + MUST(status_to_brief.emplace(403, "Forbidden"_sv)); + MUST(status_to_brief.emplace(404, "Not Found"_sv)); + MUST(status_to_brief.emplace(405, "Method Not Allowed"_sv)); + MUST(status_to_brief.emplace(406, "Not Acceptable"_sv)); + MUST(status_to_brief.emplace(407, "Proxy Authentication Required"_sv)); + MUST(status_to_brief.emplace(408, "Request Timeout"_sv)); + MUST(status_to_brief.emplace(409, "Conflict"_sv)); + MUST(status_to_brief.emplace(410, "Gone"_sv)); + MUST(status_to_brief.emplace(411, "Length Required"_sv)); + MUST(status_to_brief.emplace(412, "Precondition Failed"_sv)); + MUST(status_to_brief.emplace(413, "Payload Too Large"_sv)); + MUST(status_to_brief.emplace(414, "URI Too Long"_sv)); + MUST(status_to_brief.emplace(415, "Unsupported Media Type"_sv)); + MUST(status_to_brief.emplace(416, "Range Not Satisfiable"_sv)); + MUST(status_to_brief.emplace(417, "Expectation Failed"_sv)); + MUST(status_to_brief.emplace(418, "I'm a teapot"_sv)); + MUST(status_to_brief.emplace(421, "Misdirected Request"_sv)); + MUST(status_to_brief.emplace(422, "Unprocessable Content (WebDAV)"_sv)); + MUST(status_to_brief.emplace(423, "Locked (WebDAV)"_sv)); + MUST(status_to_brief.emplace(424, "Failed Dependency (WebDAV)"_sv)); + MUST(status_to_brief.emplace(425, "Too Early Experimental"_sv)); + MUST(status_to_brief.emplace(426, "Upgrade Required"_sv)); + MUST(status_to_brief.emplace(428, "Precondition Required"_sv)); + MUST(status_to_brief.emplace(429, "Too Many Requests"_sv)); + MUST(status_to_brief.emplace(431, "Request Header Fields Too Large"_sv)); + MUST(status_to_brief.emplace(451, "Unavailable For Legal Reasons"_sv)); + + MUST(status_to_brief.emplace(500, "Internal Server Error"_sv)); + MUST(status_to_brief.emplace(501, "Not Implemented"_sv)); + MUST(status_to_brief.emplace(502, "Bad Gateway"_sv)); + MUST(status_to_brief.emplace(503, "Service Unavailable"_sv)); + MUST(status_to_brief.emplace(504, "Gateway Timeout"_sv)); + MUST(status_to_brief.emplace(505, "HTTP Version Not Supported"_sv)); + MUST(status_to_brief.emplace(506, "Variant Also Negotiates"_sv)); + MUST(status_to_brief.emplace(507, "Insufficient Storage (WebDAV)"_sv)); + MUST(status_to_brief.emplace(508, "Loop Detected (WebDAV)"_sv)); + MUST(status_to_brief.emplace(510, "Not Extended"_sv)); + MUST(status_to_brief.emplace(511, "Network Authentication Required"_sv)); + } + + auto it = status_to_brief.find(status); + if (it == status_to_brief.end()) + return "unknown"_sv; + return it->value; +} + +static BAN::StringView extension_to_mime(BAN::StringView extension) +{ + static BAN::HashMap extension_to_mime; + if (extension_to_mime.empty()) + { + MUST(extension_to_mime.emplace(".aac"_sv, "audio/aac"_sv)); + MUST(extension_to_mime.emplace(".abw"_sv, "application/x-abiword"_sv)); + MUST(extension_to_mime.emplace(".apng"_sv, "image/apng"_sv)); + MUST(extension_to_mime.emplace(".arc"_sv, "application/x-freearc"_sv)); + MUST(extension_to_mime.emplace(".avif"_sv, "image/avif"_sv)); + MUST(extension_to_mime.emplace(".avi"_sv, "video/x-msvideo"_sv)); + MUST(extension_to_mime.emplace(".azw"_sv, "application/vnd.amazon.ebook"_sv)); + MUST(extension_to_mime.emplace(".bin"_sv, "application/octet-stream"_sv)); + MUST(extension_to_mime.emplace(".bmp"_sv, "image/bmp"_sv)); + MUST(extension_to_mime.emplace(".bz"_sv, "application/x-bzip"_sv)); + MUST(extension_to_mime.emplace(".bz2"_sv, "application/x-bzip2"_sv)); + MUST(extension_to_mime.emplace(".cda"_sv, "application/x-cdf"_sv)); + MUST(extension_to_mime.emplace(".csh"_sv, "application/x-csh"_sv)); + MUST(extension_to_mime.emplace(".css"_sv, "text/css"_sv)); + MUST(extension_to_mime.emplace(".csv"_sv, "text/csv"_sv)); + MUST(extension_to_mime.emplace(".doc"_sv, "application/msword"_sv)); + MUST(extension_to_mime.emplace(".docx"_sv, "application/vnd.openxmlformats-officedocument.wordprocessingml.document"_sv)); + MUST(extension_to_mime.emplace(".eot"_sv, "application/vnd.ms-fontobject"_sv)); + MUST(extension_to_mime.emplace(".epub"_sv, "application/epub+zip"_sv)); + MUST(extension_to_mime.emplace(".gz"_sv, "application/gzip"_sv)); + MUST(extension_to_mime.emplace(".gif"_sv, "image/gif"_sv)); + MUST(extension_to_mime.emplace(".htm"_sv, "text/html"_sv)); + MUST(extension_to_mime.emplace(".html"_sv, "text/html"_sv)); + MUST(extension_to_mime.emplace(".ico"_sv, "image/vnd.microsoft.icon"_sv)); + MUST(extension_to_mime.emplace(".ics"_sv, "text/calendar"_sv)); + MUST(extension_to_mime.emplace(".jar"_sv, "application/java-archive"_sv)); + MUST(extension_to_mime.emplace(".jpeg"_sv, "image/jpeg"_sv)); + MUST(extension_to_mime.emplace(".jpg"_sv, "image/jpeg"_sv)); + MUST(extension_to_mime.emplace(".js"_sv, "text/javascript"_sv)); + MUST(extension_to_mime.emplace(".json"_sv, "application/json"_sv)); + MUST(extension_to_mime.emplace(".jsonld"_sv, "application/ld+json"_sv)); + MUST(extension_to_mime.emplace(".mid"_sv, "audio/midi, audio/x-midi"_sv)); + MUST(extension_to_mime.emplace(".midi"_sv, "audio/midi, audio/x-midi"_sv)); + MUST(extension_to_mime.emplace(".mjs"_sv, "text/javascript"_sv)); + MUST(extension_to_mime.emplace(".mp3"_sv, "audio/mpeg"_sv)); + MUST(extension_to_mime.emplace(".mp4"_sv, "video/mp4"_sv)); + MUST(extension_to_mime.emplace(".mpeg"_sv, "video/mpeg"_sv)); + MUST(extension_to_mime.emplace(".mpkg"_sv, "application/vnd.apple.installer+xml"_sv)); + MUST(extension_to_mime.emplace(".odp"_sv, "application/vnd.oasis.opendocument.presentation"_sv)); + MUST(extension_to_mime.emplace(".ods"_sv, "application/vnd.oasis.opendocument.spreadsheet"_sv)); + MUST(extension_to_mime.emplace(".odt"_sv, "application/vnd.oasis.opendocument.text"_sv)); + MUST(extension_to_mime.emplace(".oga"_sv, "audio/ogg"_sv)); + MUST(extension_to_mime.emplace(".ogv"_sv, "video/ogg"_sv)); + MUST(extension_to_mime.emplace(".ogx"_sv, "application/ogg"_sv)); + MUST(extension_to_mime.emplace(".opus"_sv, "audio/ogg"_sv)); + MUST(extension_to_mime.emplace(".otf"_sv, "font/otf"_sv)); + MUST(extension_to_mime.emplace(".png"_sv, "image/png"_sv)); + MUST(extension_to_mime.emplace(".pdf"_sv, "application/pdf"_sv)); + MUST(extension_to_mime.emplace(".php"_sv, "application/x-httpd-php"_sv)); + MUST(extension_to_mime.emplace(".ppt"_sv, "application/vnd.ms-powerpoint"_sv)); + MUST(extension_to_mime.emplace(".pptx"_sv, "application/vnd.openxmlformats-officedocument.presentationml.presentation"_sv)); + MUST(extension_to_mime.emplace(".rar"_sv, "application/vnd.rar"_sv)); + MUST(extension_to_mime.emplace(".rtf"_sv, "application/rtf"_sv)); + MUST(extension_to_mime.emplace(".sh"_sv, "application/x-sh"_sv)); + MUST(extension_to_mime.emplace(".svg"_sv, "image/svg+xml"_sv)); + MUST(extension_to_mime.emplace(".tar"_sv, "application/x-tar"_sv)); + MUST(extension_to_mime.emplace(".tif"_sv, "image/tiff"_sv)); + MUST(extension_to_mime.emplace(".tiff"_sv, "image/tiff"_sv)); + MUST(extension_to_mime.emplace(".ts"_sv, "video/mp2t"_sv)); + MUST(extension_to_mime.emplace(".ttf"_sv, "font/ttf"_sv)); + MUST(extension_to_mime.emplace(".txt"_sv, "text/plain"_sv)); + MUST(extension_to_mime.emplace(".vsd"_sv, "application/vnd.visio"_sv)); + MUST(extension_to_mime.emplace(".wav"_sv, "audio/wav"_sv)); + MUST(extension_to_mime.emplace(".weba"_sv, "audio/webm"_sv)); + MUST(extension_to_mime.emplace(".webm"_sv, "video/webm"_sv)); + MUST(extension_to_mime.emplace(".webp"_sv, "image/webp"_sv)); + MUST(extension_to_mime.emplace(".woff"_sv, "font/woff"_sv)); + MUST(extension_to_mime.emplace(".woff2"_sv, "font/woff2"_sv)); + MUST(extension_to_mime.emplace(".xhtml"_sv, "application/xhtml+xml"_sv)); + MUST(extension_to_mime.emplace(".xls"_sv, "application/vnd.ms-excel"_sv)); + MUST(extension_to_mime.emplace(".xlsx"_sv, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"_sv)); + MUST(extension_to_mime.emplace(".xml"_sv, "application/xml"_sv)); + MUST(extension_to_mime.emplace(".xul"_sv, "application/vnd.mozilla.xul+xml"_sv)); + MUST(extension_to_mime.emplace(".zip"_sv, "application/zip"_sv)); + MUST(extension_to_mime.emplace(".3gp"_sv, "video/3gpp"_sv)); + MUST(extension_to_mime.emplace(".3g2"_sv, "video/3gpp2"_sv)); + MUST(extension_to_mime.emplace(".7z"_sv, "application/x-7z-compressed"_sv)); + } + + auto it = extension_to_mime.find(extension); + if (it == extension_to_mime.end()) + return "application/octet-stream"_sv; + return it->value; +} diff --git a/userspace/http-server/HTTPServer.h b/userspace/http-server/HTTPServer.h new file mode 100644 index 00000000..7d46abf3 --- /dev/null +++ b/userspace/http-server/HTTPServer.h @@ -0,0 +1,49 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +struct HTTPHeader +{ + BAN::StringView name; + BAN::StringView value; +}; + +struct HTTPRequest +{ + BAN::StringView method; + BAN::StringView path; + BAN::StringView version; + + BAN::Vector headers; + BAN::ConstByteSpan body; +}; + +class HTTPServer +{ +public: + HTTPServer(); + ~HTTPServer(); + + BAN::ErrorOr initialize(BAN::StringView root, BAN::IPv4Address ip, int port); + void start(); + + BAN::StringView web_root() const { return m_web_root.sv(); } + +private: + BAN::ErrorOr get_http_request(BAN::Vector& data); + BAN::ErrorOr send_http_response(int fd, unsigned status, BAN::ConstByteSpan, BAN::StringView mime); + BAN::ErrorOr handle_request(int fd, BAN::Vector& data); + // Returns false if the connection should be closed + bool handle_all_requests(int fd, BAN::Vector& data); + +private: + BAN::String m_web_root; + + int m_listen_socket { -1 }; + BAN::HashMap> m_client_data; +}; diff --git a/userspace/http-server/main.cpp b/userspace/http-server/main.cpp index e938d999..ad5513f2 100644 --- a/userspace/http-server/main.cpp +++ b/userspace/http-server/main.cpp @@ -1,110 +1,73 @@ -#include +#include "HTTPServer.h" #include #include #include -#include -#include -#include -#include -int main() +int usage(const char* argv0, int ret) { - int socket = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); - if (socket == -1) + FILE* fout = (ret == 0) ? stdout : stderr; + fprintf(fout, "usage: %s [OPTIONS]...\n", argv0); + fprintf(fout, " -h, --help show this message and exit\n"); + fprintf(fout, " -r, --root web root directory\n"); + fprintf(fout, " -b, --bind local address to bind\n"); + fprintf(fout, " -p, --port local port to bind\n"); + return ret; +} + +int main(int argc, char** argv) +{ + BAN::StringView root = "/var/www"_sv; + BAN::IPv4Address bind = INADDR_ANY; + uint16_t port = 80; + + for (int i = 1; i < argc; i++) { - perror("socket"); - return 1; - } - - sockaddr_in addr; - addr.sin_family = AF_INET; - addr.sin_port = htons(8080); - addr.sin_addr.s_addr = INADDR_ANY; - if (bind(socket, (sockaddr*)&addr, sizeof(addr)) == -1) - { - perror("bind"); - return 1; - } - - if (listen(socket, SOMAXCONN) == -1) - { - perror("listen"); - return 1; - } - - printf("server started\n"); - - BAN::Vector clients; - - char buffer[1024]; - while (true) - { - int max_sock = socket; - - fd_set fds; - FD_ZERO(&fds); - FD_SET(socket, &fds); - for (int client : clients) + if (strcmp(argv[i], "-r") == 0 || strcmp(argv[i], "--root") == 0) { - FD_SET(client, &fds); - max_sock = BAN::Math::max(client, max_sock); - } - - if (select(max_sock + 1, &fds, nullptr, nullptr, nullptr) == -1) - { - perror("select"); - break; - } - - if (FD_ISSET(socket, &fds)) - { - int client = accept(socket, nullptr, nullptr); - if (client == -1) - { - perror("accept"); - continue; - } - - printf("client %d connected\n", client); - - MUST(clients.push_back(client)); - } - - for (size_t i = 0; i < clients.size();) - { - if (!FD_ISSET(clients[i], &fds)) - { - i++; - continue; - } - - ssize_t nrecv = recv(clients[i], buffer, sizeof(buffer), 0); - if (nrecv < 0) - perror("recv"); - if (nrecv <= 0) - { - printf("%d disconnected\n", clients[i]); - close(clients[i]); - clients.remove(i); - continue; - } - - write(STDOUT_FILENO, buffer, nrecv); - - strcpy(buffer, "HTTP/1.1 200 OK\r\nContent-Length: 13\r\nConnection: close\r\n\r\nHello, world!"); - ssize_t nsend = send(clients[i], buffer, strlen(buffer), 0); - if (nsend < 0) - perror("send"); - if (nsend <= 0) - { - printf("%d disconnected\n", clients[i]); - close(clients[i]); - clients.remove(i); - continue; - } - + if (i + 1 >= argc) + return usage(argv[0], 1); + root = argv[i + 1]; i++; } + else if (strcmp(argv[i], "-b") == 0 || strcmp(argv[i], "--bind") == 0) + { + if (i + 1 >= argc) + return usage(argv[0], 1); + bind = inet_addr(argv[i + 1]); + if (bind.raw == (in_addr_t)(-1)) + return usage(argv[0], 1); + i++; + } + else if (strcmp(argv[i], "-p") == 0 || strcmp(argv[i], "--port") == 0) + { + if (i + 1 >= argc) + return usage(argv[0], 1); + char* end = NULL; + errno = 0; + unsigned long value = strtoul(argv[i + 1], &end, 10); + if (*end || value > 0xFFFF || errno) + return usage(argv[0], 1); + port = value; + i++; + } + else if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) + { + return usage(argv[0], 0); + } + else + { + return usage(argv[0], 1); + } } + + HTTPServer server; + if (auto ret = server.initialize(root, bind, port); ret.is_error()) + { + fprintf(stderr, "Could not initialize server: %s\n", strerror(ret.error().get_error_code())); + return 1; + } + + BAN::Formatter::println(putchar, "Server started on {}:{} at {}", bind, port, server.web_root()); + server.start(); }