Skip to content

Commit

Permalink
feat: static file server
Browse files Browse the repository at this point in the history
this lets you host web apps out of
:9993/app/{app_name}
:9993/app/{other_app}

from $ZT_HOME/app/{app_name}
  • Loading branch information
laduke committed Mar 11, 2024
1 parent 6be0e67 commit fe39db9
Show file tree
Hide file tree
Showing 3 changed files with 119 additions and 5 deletions.
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<app-path>

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 '<html><meta charset=utf-8><title>appA</title><body><h1>hello world A' | sudo tee app/appA/index.html
echo '<html><meta charset=utf-8><title>app2</title><body><h1>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).
9 changes: 5 additions & 4 deletions controller/EmbeddedNetworkController.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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);
Expand Down
95 changes: 94 additions & 1 deletion service/OneService.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::string> 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) {
Expand All @@ -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")) {
Expand Down Expand Up @@ -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")) {
Expand Down

0 comments on commit fe39db9

Please sign in to comment.