// Copyright (c) FIRST and other WPILib contributors. // Open Source Software; you can modify and/or share it under the terms of // the WPILib BSD license file in the root directory of this project. #include "wpinet/WebServer.h" #ifndef _WIN32 #include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include "wpinet/EventLoopRunner.h" #include "wpinet/HttpServerConnection.h" #include "wpinet/HttpUtil.h" #include "wpinet/UrlParser.h" #include "wpinet/raw_uv_ostream.h" #include "wpinet/uv/GetAddrInfo.h" #include "wpinet/uv/Stream.h" #include "wpinet/uv/Tcp.h" #include "wpinet/uv/Timer.h" using namespace wpi; namespace { class MyHttpConnection : public wpi::HttpServerConnection, public std::enable_shared_from_this { public: explicit MyHttpConnection(std::shared_ptr stream, std::string_view path) : HttpServerConnection{std::move(stream)}, m_path{path} {} protected: void ProcessRequest() override; void SendFileResponse(int code, std::string_view codeText, std::string_view contentType, fs::path filename, std::string_view extraHeader = {}); std::string m_path; }; } // namespace #ifndef _WIN32 namespace { class SendfileReq : public uv::RequestImpl { public: SendfileReq(uv_file out, uv_file in, int64_t inOffset, size_t len) : m_out(out), m_in(in), m_inOffset(inOffset), m_len(len) { error = [this](uv::Error err) { GetLoop().error(err); }; } uv::Loop& GetLoop() const { return *static_cast(GetRaw()->loop->data); } int Send(uv::Loop& loop) { int err = uv_fs_sendfile(loop.GetRaw(), GetRaw(), m_out, m_in, m_inOffset, m_len, [](uv_fs_t* req) { auto& h = *static_cast(req->data); if (req->result < 0) { h.ReportError(req->result); h.complete(); h.Release(); return; } h.m_inOffset += req->result; h.m_len -= req->result; if (h.m_len == 0) { // done h.complete(); h.Release(); // this is always a one-shot return; } // need to send more h.Send(h.GetLoop()); }); if (err < 0) { ReportError(err); complete(); } return err; } wpi::sig::Signal<> complete; private: uv_file m_out; uv_file m_in; int64_t m_inOffset; size_t m_len; }; } // namespace static void Sendfile(uv::Loop& loop, uv_file out, uv_file in, int64_t inOffset, size_t len, std::function complete) { auto req = std::make_shared(out, in, inOffset, len); if (complete) { req->complete.connect(complete); } int err = req->Send(loop); if (err >= 0) { req->Keep(); } } #endif static std::string_view GetMimeType(std::string_view ext) { static const wpi::StringMap map{ {"css", "text/css"}, {"csv", "text/csv"}, {"gif", "image/gif"}, {"htm", "text/html"}, {"html", "text/html"}, {"ico", "image/vnd.microsoft.icon"}, {"jar", "application/java-archive"}, {"jpeg", "image/jpeg"}, {"jpg", "image/jpeg"}, {"js", "text/javascript"}, {"json", "text/json"}, {"mjs", "text/javascript"}, {"pdf", "application/pdf"}, {"png", "image/png"}, {"sh", "application/x-sh"}, {"svg", "image/svg+xml"}, {"txt", "text/plain"}, {"webp", "image/webp"}, {"xhtml", "application/xhtml+xml"}, {"xml", "application/xml"}, {"zip", "application/zip"}, }; auto it = map.find(ext); if (it == map.end()) { return "application/octet-stream"; } return it->second; } void MyHttpConnection::SendFileResponse(int code, std::string_view codeText, std::string_view contentType, fs::path filename, std::string_view extraHeader) { #ifdef _WIN32 auto membuf = wpi::MemoryBuffer::GetFile(filename.string()); if (!membuf) { SendError(404); return; } wpi::SmallVector toSend; wpi::raw_uv_ostream os{toSend, 4096}; BuildHeader(os, code, codeText, contentType, (*membuf)->size(), extraHeader); SendData(os.bufs(), false); auto buf = (*membuf)->GetBuffer(); m_stream.Write( {{buf}}, [closeAfter = !m_keepAlive, stream = &m_stream, membuf = std::shared_ptr{std::move(*membuf)}](auto, uv::Error) { if (closeAfter) { stream->Close(); } }); #else // open file std::error_code ec; auto infile = fs::OpenFileForRead(filename, ec); if (ec) { SendError(404); return; } int infd = fs::FileToFd(infile, ec, fs::OF_None); if (ec) { fs::CloseFile(infile); SendError(404); return; } // get file size auto size = fs::file_size(filename, ec); if (ec) { SendError(404); ::close(infd); return; } uv_os_fd_t outfd; int err = uv_fileno(m_stream.GetRawHandle(), &outfd); if (err < 0) { m_stream.GetLoopRef().ReportError(err); SendError(404); ::close(infd); return; } wpi::SmallVector toSend; wpi::raw_uv_ostream os{toSend, 4096}; BuildHeader(os, code, codeText, contentType, size, extraHeader); SendData(os.bufs(), false); // close after write completes if we aren't keeping alive // since we're using sendfile, set socket to blocking m_stream.SetBlocking(true); Sendfile(m_stream.GetLoopRef(), outfd, infd, 0, size, [infd, closeAfter = !m_keepAlive, stream = &m_stream] { ::close(infd); if (closeAfter) { stream->Close(); } else { stream->SetBlocking(false); } }); #endif } void MyHttpConnection::ProcessRequest() { // fmt::print(stderr, "HTTP request: '{}'\n", m_request.GetUrl()); wpi::UrlParser url{m_request.GetUrl(), m_request.GetMethod() == wpi::HTTP_CONNECT}; if (!url.IsValid()) { // failed to parse URL SendError(400); return; } std::string_view path; if (url.HasPath()) { path = url.GetPath(); } // fmt::print(stderr, "path: \"{}\"\n", path); wpi::SmallString<128> pathBuf; bool error; path = UnescapeURI(path, pathBuf, &error); if (error) { SendError(400); return; } std::string_view query; if (url.HasQuery()) { query = url.GetQuery(); } // fmt::print(stderr, "query: \"{}\"\n", query); HttpQueryMap qmap{query}; const bool isGET = m_request.GetMethod() == wpi::HTTP_GET; if (isGET && wpi::starts_with(path, '/') && !wpi::contains(path, "..")) { fs::path fullpath = fmt::format("{}{}", m_path, path); std::error_code ec; bool isdir = fs::is_directory(fullpath, ec); if (isdir) { if (!wpi::ends_with(path, '/')) { // redirect to trailing / location SendResponse(301, "Moved Permanently", "text/plain", "", fmt::format("Location: {}/\r\n\r\n", path)); return; } // generate directory listing wpi::SmallString<64> formatBuf; fs::path indexpath = fs::path{fullpath} / "index.html"; if (qmap.Get("format", formatBuf).value_or("") == "json") { wpi::json dirs = wpi::json::array(); wpi::json files = wpi::json::array(); for (auto&& entry : fs::directory_iterator{fullpath}) { bool subdir = entry.is_directory(ec); std::string name = entry.path().filename().string(); if (subdir) { dirs.emplace_back(wpi::json{{"name", std::move(name)}}); } else { files.emplace_back( wpi::json{{"name", std::move(name)}, {"size", subdir ? 0 : entry.file_size(ec)}}); } } SendResponse( 200, "OK", "text/json", wpi::json{{"dirs", std::move(dirs)}, {"files", std::move(files)}} .dump()); } else if (fs::exists(indexpath)) { SendFileResponse(200, "OK", GetMimeType("html"), indexpath, "Content-Disposition: filename=\"index.html\"\r\n"); } else { wpi::StringMap dirs; wpi::StringMap files; for (auto&& entry : fs::directory_iterator{fullpath}) { bool subdir = entry.is_directory(ec); std::string name = entry.path().filename().string(); wpi::SmallString<128> nameUriBuf, nameHtmlBuf; if (subdir) { dirs.emplace( name, fmt::format( "{}/", EscapeURI(name, nameUriBuf), EscapeHTML(name, nameHtmlBuf))); } else { files.emplace( name, fmt::format( "{}{}", EscapeURI(name, nameUriBuf), EscapeHTML(name, nameHtmlBuf), entry.file_size(ec))); } } std::string html = fmt::format( "{}" "\n", path); for (auto&& str : dirs) { html += str.second; } for (auto&& str : files) { html += str.second; } html += "
NameSize
"; SendResponse(200, "OK", "text/html", html); } } else { wpi::SmallString<128> extraHeadersBuf; wpi::raw_svector_ostream os{extraHeadersBuf}; os << "Content-Disposition: filename=\""; os.write_escaped(fullpath.filename().string()); os << "\"\r\n"; SendFileResponse(200, "OK", GetMimeType(wpi::rsplit(path, '.').second), fullpath, os.str()); } } else { SendError(404, "Resource not found"); } } struct WebServer::Impl { public: EventLoopRunner runner; DenseMap> servers; }; WebServer::WebServer() : m_impl{new Impl} {} WebServer& WebServer::GetInstance() { static WebServer instance; return instance; } void WebServer::Start(unsigned int port, std::string_view path) { m_impl->runner.ExecSync([&](uv::Loop& loop) { auto server = uv::Tcp::Create(loop); if (!server) { wpi::print(stderr, "WebServer: Creating server failed\n"); return; } // bind to local port server->Bind("", port); // when we get a connection, accept it server->connection.connect( [serverPtr = server.get(), path = std::string{path}] { auto client = serverPtr->Accept(); if (!client) { wpi::print(stderr, "WebServer: Connecting to client failed\n"); return; } // close on error client->error.connect([clientPtr = client.get()](uv::Error err) { clientPtr->Close(); }); auto conn = std::make_shared(client, path); client->SetData(conn); }); // start listening for incoming connections server->Listen(); m_impl->servers[port] = server; }); } void WebServer::Stop(unsigned int port) { m_impl->runner.ExecSync([&](uv::Loop& loop) { if (auto server = m_impl->servers.lookup(port).lock()) { server->Close(); m_impl->servers.erase(port); } }); }