From 2fc8877d2a5334905e875457021de4bdbd53c317 Mon Sep 17 00:00:00 2001 From: Peter Johnson Date: Sat, 7 Dec 2024 23:59:35 -0800 Subject: [PATCH 01/14] [wpinet] Add simple web server --- .../java/edu/wpi/first/net/WPINetJNI.java | 16 ++++++ .../java/edu/wpi/first/net/WebServer.java | 32 +++++++++++ wpinet/src/main/native/cpp/jni/WPINetJNI.cpp | 26 +++++++++ .../main/native/include/wpinet/WebServer.h | 56 +++++++++++++++++++ 4 files changed, 130 insertions(+) create mode 100644 wpinet/src/main/java/edu/wpi/first/net/WebServer.java create mode 100644 wpinet/src/main/native/include/wpinet/WebServer.h diff --git a/wpinet/src/main/java/edu/wpi/first/net/WPINetJNI.java b/wpinet/src/main/java/edu/wpi/first/net/WPINetJNI.java index 084f0220f54..a4bfadf2b36 100644 --- a/wpinet/src/main/java/edu/wpi/first/net/WPINetJNI.java +++ b/wpinet/src/main/java/edu/wpi/first/net/WPINetJNI.java @@ -80,6 +80,22 @@ public static synchronized void forceLoad() throws IOException { */ public static native void removePortForwarder(int port); + /** + * Create a web server at the given port. Note that local ports less than 1024 won't work as a + * normal user. + * + * @param port local port number + * @param path local path to document root + */ + public static native void startWebServer(int port, String path); + + /** + * Stop web server running at the given port. + * + * @param port local port number + */ + public static native void stopWebServer(int port); + /** * Creates a MulticastServiceAnnouncer. * diff --git a/wpinet/src/main/java/edu/wpi/first/net/WebServer.java b/wpinet/src/main/java/edu/wpi/first/net/WebServer.java new file mode 100644 index 00000000000..dfe30a081ac --- /dev/null +++ b/wpinet/src/main/java/edu/wpi/first/net/WebServer.java @@ -0,0 +1,32 @@ +// 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. + +package edu.wpi.first.net; + +/** A web server using the HTTP protocol. */ +public final class WebServer { + private WebServer() { + throw new UnsupportedOperationException("This is a utility class!"); + } + + /** + * Create a web server at the given port. Note that local ports less than 1024 won't work as a + * normal user. + * + * @param port local port number + * @param path local path to document root + */ + public static void start(int port, String path) { + WPINetJNI.startWebServer(port, path); + } + + /** + * Stop web server running at the given port. + * + * @param port local port number + */ + public static void stop(int port) { + WPINetJNI.stopWebServer(port); + } +} diff --git a/wpinet/src/main/native/cpp/jni/WPINetJNI.cpp b/wpinet/src/main/native/cpp/jni/WPINetJNI.cpp index fb59c4d9598..deb4fa53961 100644 --- a/wpinet/src/main/native/cpp/jni/WPINetJNI.cpp +++ b/wpinet/src/main/native/cpp/jni/WPINetJNI.cpp @@ -16,6 +16,7 @@ #include "wpinet/MulticastServiceAnnouncer.h" #include "wpinet/MulticastServiceResolver.h" #include "wpinet/PortForwarder.h" +#include "wpinet/WebServer.h" using namespace wpi::java; @@ -80,6 +81,31 @@ Java_edu_wpi_first_net_WPINetJNI_removePortForwarder wpi::PortForwarder::GetInstance().Remove(port); } +/* + * Class: edu_wpi_first_net_WPINetJNI + * Method: startWebserver + * Signature: (ILjava/lang/String;I)V + */ +JNIEXPORT void JNICALL +Java_edu_wpi_first_net_WPINetJNI_startWebServer + (JNIEnv* env, jclass, jint port, jstring path) +{ + wpi::WebServer::GetInstance().Add(static_cast(port), + JStringRef{env, path}.str()); +} + +/* + * Class: edu_wpi_first_net_WPINetJNI + * Method: stopWebServer + * Signature: (I)V + */ +JNIEXPORT void JNICALL +Java_edu_wpi_first_net_WPINetJNI_stopWebServer + (JNIEnv* env, jclass, jint port) +{ + wpi::WebServer::GetInstance().Remove(port); +} + /* * Class: edu_wpi_first_net_WPINetJNI * Method: createMulticastServiceAnnouncer diff --git a/wpinet/src/main/native/include/wpinet/WebServer.h b/wpinet/src/main/native/include/wpinet/WebServer.h new file mode 100644 index 00000000000..e6d88d1ffb9 --- /dev/null +++ b/wpinet/src/main/native/include/wpinet/WebServer.h @@ -0,0 +1,56 @@ +// 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. + +#ifndef WPINET_WEBSERVER_H_ +#define WPINET_WEBSERVER_H_ + +#pragma once + +#include +#include + +namespace wpi { + +/** + * A web server using the HTTP protocol. + */ +class WebServer { + public: + WebServer(const WebServer&) = delete; + WebServer& operator=(const WebServer&) = delete; + + /** + * Get an instance of the WebServer class. + * + * This is a singleton to guarantee that there is only a single instance + * regardless of how many times GetInstance is called. + */ + static WebServer& GetInstance(); + + /** + * Create a web server at the given port. + * Note that local ports less than 1024 won't work as a normal user. + * + * @param port local port number + * @param path local path to document root + */ + void Start(unsigned int port, std::string_view path); + + /** + * Stop web server running at the given port. + * + * @param port local port number + */ + void Stop(unsigned int port); + + private: + WebServer(); + + struct Impl; + std::unique_ptr m_impl; +}; + +} // namespace wpi + +#endif // WPINET_WEBSERVER_H_ From b5064bd95c3eae328e2cc267d3f32fd463c6555f Mon Sep 17 00:00:00 2001 From: Peter Johnson Date: Sun, 8 Dec 2024 13:52:55 -0800 Subject: [PATCH 02/14] Add implementation --- wpinet/src/main/native/cpp/WebServer.cpp | 281 +++++++++++++++++++++++ 1 file changed, 281 insertions(+) create mode 100644 wpinet/src/main/native/cpp/WebServer.cpp diff --git a/wpinet/src/main/native/cpp/WebServer.cpp b/wpinet/src/main/native/cpp/WebServer.cpp new file mode 100644 index 00000000000..cf73a0372a2 --- /dev/null +++ b/wpinet/src/main/native/cpp/WebServer.cpp @@ -0,0 +1,281 @@ +// 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" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "wpinet/EventLoopRunner.h" +#include "wpinet/HttpServerConnection.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, std::string_view filename, + std::string_view extraHeader = {}); + + std::string m_path; +}; + +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(); + } +} + +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, + std::string_view filename, + std::string_view extraHeader) { + // 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); + } + }); +} + +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); + + std::string_view query; + if (url.HasQuery()) { + query = url.GetQuery(); + } + // fmt::print(stderr, "query: \"{}\"\n", query); + + const bool isGET = m_request.GetMethod() == wpi::HTTP_GET; + if (isGET && wpi::starts_with(path, "/") && !wpi::contains(path, "..")) { + SendFileResponse(200, "OK", GetMimeType(wpi::rsplit(path, '.').second), + fmt::format("{}{}", m_path, path)); + } 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, "PortForwarder: 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); + } + }); +} From f14fb16d8d3b7ee1f60b15bd3de6e78b760ab48b Mon Sep 17 00:00:00 2001 From: Peter Johnson Date: Sun, 8 Dec 2024 13:54:22 -0800 Subject: [PATCH 03/14] Fix error message --- wpinet/src/main/native/cpp/WebServer.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wpinet/src/main/native/cpp/WebServer.cpp b/wpinet/src/main/native/cpp/WebServer.cpp index cf73a0372a2..e14bb82b7fb 100644 --- a/wpinet/src/main/native/cpp/WebServer.cpp +++ b/wpinet/src/main/native/cpp/WebServer.cpp @@ -239,7 +239,7 @@ 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, "PortForwarder: Creating server failed\n"); + wpi::print(stderr, "WebServer: Creating server failed\n"); return; } From 1b7faa99a417ba7031a2bd37b88fc997fdf908b7 Mon Sep 17 00:00:00 2001 From: Peter Johnson Date: Sun, 8 Dec 2024 22:10:45 -0800 Subject: [PATCH 04/14] Fix build --- wpinet/src/main/native/cpp/jni/WPINetJNI.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/wpinet/src/main/native/cpp/jni/WPINetJNI.cpp b/wpinet/src/main/native/cpp/jni/WPINetJNI.cpp index deb4fa53961..69385ca16aa 100644 --- a/wpinet/src/main/native/cpp/jni/WPINetJNI.cpp +++ b/wpinet/src/main/native/cpp/jni/WPINetJNI.cpp @@ -84,14 +84,14 @@ Java_edu_wpi_first_net_WPINetJNI_removePortForwarder /* * Class: edu_wpi_first_net_WPINetJNI * Method: startWebserver - * Signature: (ILjava/lang/String;I)V + * Signature: (ILjava/lang/String;)V */ JNIEXPORT void JNICALL Java_edu_wpi_first_net_WPINetJNI_startWebServer (JNIEnv* env, jclass, jint port, jstring path) { - wpi::WebServer::GetInstance().Add(static_cast(port), - JStringRef{env, path}.str()); + wpi::WebServer::GetInstance().Start(static_cast(port), + JStringRef{env, path}.str()); } /* @@ -103,7 +103,7 @@ JNIEXPORT void JNICALL Java_edu_wpi_first_net_WPINetJNI_stopWebServer (JNIEnv* env, jclass, jint port) { - wpi::WebServer::GetInstance().Remove(port); + wpi::WebServer::GetInstance().Stop(port); } /* From b7cc5ba5a9897edc7a8a8c497f4246aa2f1d553f Mon Sep 17 00:00:00 2001 From: Peter Johnson Date: Sun, 8 Dec 2024 22:26:21 -0800 Subject: [PATCH 05/14] Formatting --- wpinet/src/main/native/cpp/jni/WPINetJNI.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wpinet/src/main/native/cpp/jni/WPINetJNI.cpp b/wpinet/src/main/native/cpp/jni/WPINetJNI.cpp index 69385ca16aa..04efdab2044 100644 --- a/wpinet/src/main/native/cpp/jni/WPINetJNI.cpp +++ b/wpinet/src/main/native/cpp/jni/WPINetJNI.cpp @@ -83,7 +83,7 @@ Java_edu_wpi_first_net_WPINetJNI_removePortForwarder /* * Class: edu_wpi_first_net_WPINetJNI - * Method: startWebserver + * Method: startWebServer * Signature: (ILjava/lang/String;)V */ JNIEXPORT void JNICALL From dced4b5679a103973a20948a24b00d9b8eda292d Mon Sep 17 00:00:00 2001 From: Peter Johnson Date: Sun, 8 Dec 2024 22:27:48 -0800 Subject: [PATCH 06/14] Add missing include --- wpinet/src/main/native/cpp/WebServer.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/wpinet/src/main/native/cpp/WebServer.cpp b/wpinet/src/main/native/cpp/WebServer.cpp index e14bb82b7fb..e101a2e9200 100644 --- a/wpinet/src/main/native/cpp/WebServer.cpp +++ b/wpinet/src/main/native/cpp/WebServer.cpp @@ -4,6 +4,8 @@ #include "wpinet/WebServer.h" +#include + #include #include #include From 94b9f7b049ad2ccbbf94902341a9f68a874ff7e1 Mon Sep 17 00:00:00 2001 From: Peter Johnson Date: Mon, 9 Dec 2024 22:41:19 -0800 Subject: [PATCH 07/14] Add directory browsing --- wpinet/src/main/native/cpp/WebServer.cpp | 52 +++++++++++++++++++++--- 1 file changed, 47 insertions(+), 5 deletions(-) diff --git a/wpinet/src/main/native/cpp/WebServer.cpp b/wpinet/src/main/native/cpp/WebServer.cpp index e101a2e9200..0e94e6ff043 100644 --- a/wpinet/src/main/native/cpp/WebServer.cpp +++ b/wpinet/src/main/native/cpp/WebServer.cpp @@ -14,11 +14,13 @@ #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" @@ -39,7 +41,7 @@ class MyHttpConnection : public wpi::HttpServerConnection, protected: void ProcessRequest() override; void SendFileResponse(int code, std::string_view codeText, - std::string_view contentType, std::string_view filename, + std::string_view contentType, fs::path filename, std::string_view extraHeader = {}); std::string m_path; @@ -141,7 +143,7 @@ static std::string_view GetMimeType(std::string_view ext) { void MyHttpConnection::SendFileResponse(int code, std::string_view codeText, std::string_view contentType, - std::string_view filename, + fs::path filename, std::string_view extraHeader) { // open file std::error_code ec; @@ -214,11 +216,51 @@ void MyHttpConnection::ProcessRequest() { 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, "..")) { - SendFileResponse(200, "OK", GetMimeType(wpi::rsplit(path, '.').second), - fmt::format("{}{}", m_path, path)); + 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; + if (qmap.Get("format", formatBuf).value_or("") == "json") { + wpi::json j = wpi::json::array(); + for (auto&& entry : fs::directory_iterator{fullpath}) { + bool subdir = entry.is_directory(ec); + j.emplace_back( + wpi::json{{"filename", entry.path().filename().string()}, + {"size", subdir ? 0 : entry.file_size(ec)}, + {"directory", subdir}}); + } + SendResponse(200, "OK", "text/json", j.dump()); + } else { + std::string html = fmt::format( + "{}" + "\n", + path); + for (auto&& entry : fs::directory_iterator{fullpath}) { + bool subdir = entry.is_directory(ec); + html += fmt::format( + "", + entry.path().filename().string(), subdir ? "/" : "", + subdir ? "" : fmt::to_string(entry.file_size(ec))); + } + html += "
NameSize
{0}{1}{2}
"; + SendResponse(200, "OK", "text/html", html); + } + } else { + SendFileResponse(200, "OK", GetMimeType(wpi::rsplit(path, '.').second), + fullpath); + } } else { SendError(404, "Resource not found"); } From 32f2df2247dc653802dacee33812364c740e742d Mon Sep 17 00:00:00 2001 From: Peter Johnson Date: Tue, 10 Dec 2024 21:36:14 -0800 Subject: [PATCH 08/14] [wpinet] HttpUtil: Add EscapeHTML --- wpinet/src/main/native/cpp/HttpUtil.cpp | 16 ++++++++++++++++ wpinet/src/main/native/include/wpinet/HttpUtil.h | 5 +++++ 2 files changed, 21 insertions(+) diff --git a/wpinet/src/main/native/cpp/HttpUtil.cpp b/wpinet/src/main/native/cpp/HttpUtil.cpp index 92022ee7978..83d9ded426b 100644 --- a/wpinet/src/main/native/cpp/HttpUtil.cpp +++ b/wpinet/src/main/native/cpp/HttpUtil.cpp @@ -81,6 +81,22 @@ std::string_view EscapeURI(std::string_view str, SmallVectorImpl& buf, return {buf.data(), buf.size()}; } +std::string_view EscapeHTML(std::string_view str, SmallVectorImpl& buf) { + buf.clear(); + for (auto i = str.begin(), end = str.end(); i != end; ++i) { + if (*i == '&') { + buf.append({'&', 'a', 'm', 'p', ';'}); + } else if (*i == '<') { + buf.append({'&', 'l', 't', ';'}); + } else if (*i == '>') { + buf.append({'&', 'g', 't', ';'}); + } else { + buf.push_back(*i); + } + } + return {buf.data(), buf.size()}; +} + HttpQueryMap::HttpQueryMap(std::string_view query) { SmallVector queryElems; split(query, queryElems, '&', 100, false); diff --git a/wpinet/src/main/native/include/wpinet/HttpUtil.h b/wpinet/src/main/native/include/wpinet/HttpUtil.h index 1509a6f1a03..dca5225809e 100644 --- a/wpinet/src/main/native/include/wpinet/HttpUtil.h +++ b/wpinet/src/main/native/include/wpinet/HttpUtil.h @@ -39,6 +39,11 @@ std::string_view UnescapeURI(std::string_view str, SmallVectorImpl& buf, std::string_view EscapeURI(std::string_view str, SmallVectorImpl& buf, bool spacePlus = true); +// Escape a string for HTML output. +// @param buf Buffer for output +// @return Escaped string +std::string_view EscapeHTML(std::string_view str, SmallVectorImpl& buf); + // Parse a set of HTTP headers. Saves just the Content-Type and Content-Length // fields. // @param is Input stream From 2b135bba3dba31233af41a83c8801f4ad3483a52 Mon Sep 17 00:00:00 2001 From: Peter Johnson Date: Tue, 10 Dec 2024 21:36:31 -0800 Subject: [PATCH 09/14] Improve directory listing output --- wpinet/src/main/native/cpp/WebServer.cpp | 52 ++++++++++++++++++------ 1 file changed, 40 insertions(+), 12 deletions(-) diff --git a/wpinet/src/main/native/cpp/WebServer.cpp b/wpinet/src/main/native/cpp/WebServer.cpp index 0e94e6ff043..bfe2bfda707 100644 --- a/wpinet/src/main/native/cpp/WebServer.cpp +++ b/wpinet/src/main/native/cpp/WebServer.cpp @@ -233,26 +233,54 @@ void MyHttpConnection::ProcessRequest() { // generate directory listing wpi::SmallString<64> formatBuf; if (qmap.Get("format", formatBuf).value_or("") == "json") { - wpi::json j = wpi::json::array(); + 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); - j.emplace_back( - wpi::json{{"filename", entry.path().filename().string()}, - {"size", subdir ? 0 : entry.file_size(ec)}, - {"directory", subdir}}); + 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", j.dump()); + SendResponse( + 200, "OK", "text/json", + wpi::json{{"dirs", std::move(dirs)}, {"files", std::move(files)}} + .dump()); } 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&& entry : fs::directory_iterator{fullpath}) { - bool subdir = entry.is_directory(ec); - html += fmt::format( - "", - entry.path().filename().string(), subdir ? "/" : "", - subdir ? "" : fmt::to_string(entry.file_size(ec))); + for (auto&& str : dirs) { + html += str.second; + } + for (auto&& str : files) { + html += str.second; } html += "
NameSize
{0}{1}{2}
"; SendResponse(200, "OK", "text/html", html); From 8c5d35cd7cf5dfce30b9eae8c320a1b7c84b8342 Mon Sep 17 00:00:00 2001 From: Peter Johnson Date: Tue, 10 Dec 2024 21:37:12 -0800 Subject: [PATCH 10/14] Formatting --- wpinet/src/main/native/cpp/WebServer.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wpinet/src/main/native/cpp/WebServer.cpp b/wpinet/src/main/native/cpp/WebServer.cpp index bfe2bfda707..33821929eaf 100644 --- a/wpinet/src/main/native/cpp/WebServer.cpp +++ b/wpinet/src/main/native/cpp/WebServer.cpp @@ -14,8 +14,8 @@ #include #include #include -#include #include +#include #include #include "wpinet/EventLoopRunner.h" From 8b96b17fe06ad711e1aa6dbd2136021f912c9eec Mon Sep 17 00:00:00 2001 From: Peter Johnson Date: Sat, 14 Dec 2024 09:30:31 -0800 Subject: [PATCH 11/14] WIP Windows fix --- wpinet/src/main/native/cpp/WebServer.cpp | 27 ++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/wpinet/src/main/native/cpp/WebServer.cpp b/wpinet/src/main/native/cpp/WebServer.cpp index 33821929eaf..8c8e889c471 100644 --- a/wpinet/src/main/native/cpp/WebServer.cpp +++ b/wpinet/src/main/native/cpp/WebServer.cpp @@ -12,6 +12,7 @@ #include #include +#include #include #include #include @@ -46,7 +47,11 @@ class MyHttpConnection : public wpi::HttpServerConnection, std::string m_path; }; +} // namespace +#if 1 //def _WIN32 +#else +namespace { class SendfileReq : public uv::RequestImpl { public: SendfileReq(uv_file out, uv_file in, int64_t inOffset, size_t len) @@ -109,6 +114,7 @@ static void Sendfile(uv::Loop& loop, uv_file out, uv_file in, int64_t inOffset, req->Keep(); } } +#endif static std::string_view GetMimeType(std::string_view ext) { static const wpi::StringMap map{ @@ -145,6 +151,26 @@ void MyHttpConnection::SendFileResponse(int code, std::string_view codeText, std::string_view contentType, fs::path filename, std::string_view extraHeader) { +#if 1 // def _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); + m_stream.Write( + {{(*membuf)->GetBuffer()}}, + [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); @@ -193,6 +219,7 @@ void MyHttpConnection::SendFileResponse(int code, std::string_view codeText, stream->SetBlocking(false); } }); +#endif } void MyHttpConnection::ProcessRequest() { From 73a2124307e948f4133361b344684320724362d2 Mon Sep 17 00:00:00 2001 From: Peter Johnson Date: Sat, 14 Dec 2024 09:53:20 -0800 Subject: [PATCH 12/14] Add comment re: radio firewall --- wpinet/src/main/java/edu/wpi/first/net/WebServer.java | 3 ++- wpinet/src/main/native/include/wpinet/WebServer.h | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/wpinet/src/main/java/edu/wpi/first/net/WebServer.java b/wpinet/src/main/java/edu/wpi/first/net/WebServer.java index dfe30a081ac..e7a71a5e79a 100644 --- a/wpinet/src/main/java/edu/wpi/first/net/WebServer.java +++ b/wpinet/src/main/java/edu/wpi/first/net/WebServer.java @@ -12,7 +12,8 @@ private WebServer() { /** * Create a web server at the given port. Note that local ports less than 1024 won't work as a - * normal user. + * normal user. Also, many ports are blocked by the FRC robot radio; check the game manual for + * what is allowed through the radio firewall. * * @param port local port number * @param path local path to document root diff --git a/wpinet/src/main/native/include/wpinet/WebServer.h b/wpinet/src/main/native/include/wpinet/WebServer.h index e6d88d1ffb9..ebfb9a36b2b 100644 --- a/wpinet/src/main/native/include/wpinet/WebServer.h +++ b/wpinet/src/main/native/include/wpinet/WebServer.h @@ -30,7 +30,9 @@ class WebServer { /** * Create a web server at the given port. - * Note that local ports less than 1024 won't work as a normal user. + * Note that local ports less than 1024 won't work as a normal user. Also, + * many ports are blocked by the FRC robot radio; check the game manual for + * what is allowed through the radio firewall. * * @param port local port number * @param path local path to document root From 5766f4e218ca86f1be578763fc2a49f768ceea3e Mon Sep 17 00:00:00 2001 From: Peter Johnson Date: Sat, 14 Dec 2024 10:50:59 -0800 Subject: [PATCH 13/14] Finish Windows update --- wpinet/src/main/native/cpp/WebServer.cpp | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/wpinet/src/main/native/cpp/WebServer.cpp b/wpinet/src/main/native/cpp/WebServer.cpp index 8c8e889c471..6e0ea096afa 100644 --- a/wpinet/src/main/native/cpp/WebServer.cpp +++ b/wpinet/src/main/native/cpp/WebServer.cpp @@ -4,8 +4,6 @@ #include "wpinet/WebServer.h" -#include - #include #include #include @@ -49,8 +47,7 @@ class MyHttpConnection : public wpi::HttpServerConnection, }; } // namespace -#if 1 //def _WIN32 -#else +#ifndef _WIN32 namespace { class SendfileReq : public uv::RequestImpl { public: @@ -151,7 +148,7 @@ void MyHttpConnection::SendFileResponse(int code, std::string_view codeText, std::string_view contentType, fs::path filename, std::string_view extraHeader) { -#if 1 // def _WIN32 +#ifdef _WIN32 auto membuf = wpi::MemoryBuffer::GetFile(filename.string()); if (!membuf) { SendError(404); From 8de5188f3725196709fe95f6de0485654a3f54b8 Mon Sep 17 00:00:00 2001 From: Peter Johnson Date: Sat, 14 Dec 2024 11:10:04 -0800 Subject: [PATCH 14/14] Add back in unistd include --- wpinet/src/main/native/cpp/WebServer.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/wpinet/src/main/native/cpp/WebServer.cpp b/wpinet/src/main/native/cpp/WebServer.cpp index 6e0ea096afa..8df876ab30c 100644 --- a/wpinet/src/main/native/cpp/WebServer.cpp +++ b/wpinet/src/main/native/cpp/WebServer.cpp @@ -4,6 +4,10 @@ #include "wpinet/WebServer.h" +#ifndef _WIN32 +#include +#endif + #include #include #include