From fe39db950c1aa1ba61c58b529cf48c322feec229 Mon Sep 17 00:00:00 2001 From: travisladuke Date: Mon, 5 Feb 2024 12:38:44 -0800 Subject: [PATCH] feat: static file server this lets you host web apps out of :9993/app/{app_name} :9993/app/{other_app} from $ZT_HOME/app/{app_name} --- README.md | 20 +++++ controller/EmbeddedNetworkController.cpp | 9 ++- service/OneService.cpp | 95 +++++++++++++++++++++++- 3 files changed, 119 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index fb1935254..42eecdda0 100644 --- a/README.md +++ b/README.md @@ -175,3 +175,23 @@ Metrics are also available on disk in ZeroTier's working directory: | zt_peer_packet_errors | node_id | Counter | number of incoming packet errors from a peer | If there are other metrics you'd like to see tracked, ask us in an Issue or send us a Pull Request! + +### HTTP / App server + +There is a static http file server suitable for hosting Single Page Apps at http://localhost:9993/app/ + +Use `zerotier-cli info -j` to find your zerotier-one service's homeDir + +``` sh +cd $ZT_HOME +sudo mkdir -p app/app1 +sudo mkdir -p app/appB +echo 'appA

hello world A' | sudo tee app/appA/index.html +echo 'app2

hello world 2' | sudo tee app/app2/index.html +curl -sL http://localhost:9993/app/appA http://localhost:9993/app/app2 +``` + +Then visit [http://localhost:9993/app/app1/](http://localhost:9993/app/app1/) and [http://localhost:9993/app/appB/](http://localhost:9993/app/appB/) + +Requests to paths don't exist return the app root index.html, as is customary for SPAs. +If you want, you can write some javascript that talks to the service or controller [api](https://docs.zerotier.com/service/v1). diff --git a/controller/EmbeddedNetworkController.cpp b/controller/EmbeddedNetworkController.cpp index 9cb971a58..d9eb5f735 100644 --- a/controller/EmbeddedNetworkController.cpp +++ b/controller/EmbeddedNetworkController.cpp @@ -876,6 +876,7 @@ void EmbeddedNetworkController::configureHTTPControlPlane( std::string memberListPath2 = "/unstable/controller/network/([0-9a-fA-F]{16})/member"; std::string memberPath = "/controller/network/([0-9a-fA-F]{16})/member/([0-9a-fA-F]{10})"; + auto controllerGet = [&, setContent](const httplib::Request &req, httplib::Response &res) { char tmp[4096]; const bool dbOk = _db.isReady(); @@ -887,11 +888,11 @@ void EmbeddedNetworkController::configureHTTPControlPlane( (unsigned long long)OSUtils::now(), dbOk ? "true" : "false"); - if (!dbOk) { - res.status = 503; - } + if (!dbOk) { + res.status = 503; + } - setContent(req, res, tmp); + setContent(req, res, tmp); }; s.Get(controllerPath, controllerGet); sv6.Get(controllerPath, controllerGet); diff --git a/service/OneService.cpp b/service/OneService.cpp index eeebd90ed..32087af5a 100644 --- a/service/OneService.cpp +++ b/service/OneService.cpp @@ -1555,9 +1555,14 @@ class OneServiceImpl : public OneService std::string peerPath = "/peer/([0-9a-fA-F]{10})"; std::string statusPath = "/status"; std::string metricsPath = "/metrics"; + static std::string appUiPath = "/app"; std::vector noAuthEndpoints { "/sso", "/health" }; + static char appUiDir[16384]; + sprintf(appUiDir,"%s%s",_homePath.c_str(),appUiPath.c_str()); + + auto setContent = [=] (const httplib::Request &req, httplib::Response &res, std::string content) { if (req.has_param("jsonp")) { if (content.length() > 0) { @@ -1573,9 +1578,92 @@ class OneServiceImpl : public OneService } } }; + // + // static file server for controller-ui + // + auto ret = _controlPlane.set_mount_point(appUiPath, appUiDir); + _controlPlaneV6.set_mount_point(appUiPath, appUiDir); + if (!ret) { + fprintf(stderr, "Mounting app directory failed. Creating it. Path: %s - Dir: %s\n", appUiPath.c_str(), appUiDir); + if (!OSUtils::mkdir(appUiDir)) { + fprintf(stderr, "Could not create app directory either. Path: %s - Dir: %s\n", appUiPath.c_str(), appUiDir); + } else { + ret = _controlPlane.set_mount_point(appUiPath, appUiDir); + _controlPlaneV6.set_mount_point(appUiPath, appUiDir); + if (!ret) { + fprintf(stderr, "Really could not create and mount directory. Path: %s - Dir: %s\nWeb apps won't work.\n", appUiPath.c_str(), appUiDir); + } + } + } + + if (ret) { + // fallback to /index.html for paths that don't exist for SPAs + auto indexFallbackGet = [](const httplib::Request &req, httplib::Response &res) { + // fprintf(stderr, "fallback \n"); + + auto match = req.matches[1]; + if (match.matched) { + + // fallback + char indexHtmlPath[16384]; + sprintf(indexHtmlPath,"%s/%s/%s", appUiDir, match.str().c_str(), "index.html"); + // fprintf(stderr, "fallback path %s\n", indexHtmlPath); + + std::string indexHtml; + + if (!OSUtils::readFile(indexHtmlPath, indexHtml)) { + res.status = 500; + return; + } + + res.set_content(indexHtml.c_str(), "text/html"); + } else { + res.status = 500; + return; + } + }; + + auto slashRedirect = [](const httplib::Request &req, httplib::Response &res) { + // fprintf(stderr, "redirect \n"); + + // add .html + std::string htmlFile; + char htmlPath[16384]; + sprintf(htmlPath,"%s%s%s", appUiDir, (req.path).substr(appUiPath.length()).c_str(), ".html"); + // fprintf(stderr, "path: %s\n", htmlPath); + if (OSUtils::readFile(htmlPath, htmlFile)) { + res.set_content(htmlFile.c_str(), "text/html"); + return; + } else { + res.status = 301; + res.set_header("location", req.path + "/"); + } + }; + + // auto missingAssetGet = [&, setContent](const httplib::Request &req, httplib::Response &res) { + // fprintf(stderr, "missing \n"); + // res.status = 404; + // std::string html = "oops"; + // res.set_content(html, "text/plain"); + // res.set_header("Content-Type", "text/plain"); + // return; + // }; + // auto fix no trailing slash by adding .html or redirecting to path/ + _controlPlane.Get(appUiPath + R"((/[\w|-]+)+$)", slashRedirect); + _controlPlaneV6.Get(appUiPath + R"((/[\w|-]+)+$)", slashRedirect); - auto authCheck = [=] (const httplib::Request &req, httplib::Response &res) { + // // 404 missing assets for *.ext paths + // s.Get(appUiPath + R"(/\.\w+$)", missingAssetGet); + // sv6.Get(appUiPath + R"(/\.\w+$)", missingAssetGet); + + // fallback to index.html for unknown paths/files + _controlPlane.Get(appUiPath + R"((/[\w|-]+)(/[\w|-]+)*/$)", indexFallbackGet); + _controlPlaneV6.Get(appUiPath + R"((/[\w|-]+)(/[\w|-]+)*/$)", indexFallbackGet); + + } + + auto authCheck = [=] (const httplib::Request &req, httplib::Response &res) { if (req.path == "/metrics") { if (req.has_header("x-zt1-auth")) { @@ -1625,6 +1713,11 @@ class OneServiceImpl : public OneService isAuth = true; } + // Web Apps base path + if (req.path.rfind("/app", 0) == 0) { //starts with /app + isAuth = true; + } + if (!isAuth) { // check auth token if (req.has_header("x-zt1-auth")) {