Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

static file server #2218

Merged
merged 1 commit into from
Mar 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
100 changes: 99 additions & 1 deletion service/OneService.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -796,6 +796,7 @@ class OneServiceImpl : public OneService
bool _allowTcpFallbackRelay;
bool _forceTcpRelay;
bool _allowSecondaryPort;
bool _enableWebServer;

unsigned int _primaryPort;
unsigned int _secondaryPort;
Expand Down Expand Up @@ -1558,6 +1559,7 @@ class OneServiceImpl : public OneService

std::vector<std::string> noAuthEndpoints { "/sso", "/health" };


auto setContent = [=] (const httplib::Request &req, httplib::Response &res, std::string content) {
if (req.has_param("jsonp")) {
if (content.length() > 0) {
Expand All @@ -1574,8 +1576,98 @@ class OneServiceImpl : public OneService
}
};

//
// static file server for app ui'
//
if (_enableWebServer) {
static std::string appUiPath = "/app";
static char appUiDir[16384];
sprintf(appUiDir,"%s%s",_homePath.c_str(),appUiPath.c_str());

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 +1717,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 Expand Up @@ -2452,6 +2549,7 @@ class OneServiceImpl : public OneService
}
_allowTcpFallbackRelay = (OSUtils::jsonBool(settings["allowTcpFallbackRelay"],true) && !_node->bondController()->inUse());
_forceTcpRelay = (_forceTcpRelayTmp && !_node->bondController()->inUse());
_enableWebServer = (OSUtils::jsonBool(settings["enableWebServer"],false));

#ifdef ZT_TCP_FALLBACK_RELAY
_fallbackRelayAddress = InetAddress(OSUtils::jsonString(settings["tcpFallbackRelay"], ZT_TCP_FALLBACK_RELAY).c_str());
Expand Down
Loading