aboutsummaryrefslogtreecommitdiff
path: root/src/zenserver/frontend
diff options
context:
space:
mode:
authorStefan Boberg <[email protected]>2023-05-02 10:01:47 +0200
committerGitHub <[email protected]>2023-05-02 10:01:47 +0200
commit075d17f8ada47e990fe94606c3d21df409223465 (patch)
treee50549b766a2f3c354798a54ff73404217b4c9af /src/zenserver/frontend
parentfix: bundle shouldn't append content zip to zen (diff)
downloadzen-075d17f8ada47e990fe94606c3d21df409223465.tar.xz
zen-075d17f8ada47e990fe94606c3d21df409223465.zip
moved source directories into `/src` (#264)
* moved source directories into `/src` * updated bundle.lua for new `src` path * moved some docs, icon * removed old test trees
Diffstat (limited to 'src/zenserver/frontend')
-rw-r--r--src/zenserver/frontend/frontend.cpp128
-rw-r--r--src/zenserver/frontend/frontend.h25
-rw-r--r--src/zenserver/frontend/html/index.html59
-rw-r--r--src/zenserver/frontend/zipfs.cpp169
-rw-r--r--src/zenserver/frontend/zipfs.h26
5 files changed, 407 insertions, 0 deletions
diff --git a/src/zenserver/frontend/frontend.cpp b/src/zenserver/frontend/frontend.cpp
new file mode 100644
index 000000000..149d97924
--- /dev/null
+++ b/src/zenserver/frontend/frontend.cpp
@@ -0,0 +1,128 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#include "frontend.h"
+
+#include <zencore/endian.h>
+#include <zencore/filesystem.h>
+#include <zencore/string.h>
+
+ZEN_THIRD_PARTY_INCLUDES_START
+#if ZEN_PLATFORM_WINDOWS
+# include <Windows.h>
+#endif
+ZEN_THIRD_PARTY_INCLUDES_END
+
+namespace zen {
+
+////////////////////////////////////////////////////////////////////////////////
+HttpFrontendService::HttpFrontendService(std::filesystem::path Directory) : m_Directory(Directory)
+{
+ std::filesystem::path SelfPath = GetRunningExecutablePath();
+
+ // Locate a .zip file appended onto the end of this binary
+ IoBuffer SelfBuffer = IoBufferBuilder::MakeFromFile(SelfPath);
+ m_ZipFs = ZipFs(std::move(SelfBuffer));
+
+#if ZEN_BUILD_DEBUG
+ if (!Directory.empty())
+ {
+ return;
+ }
+
+ std::error_code ErrorCode;
+ auto Path = SelfPath;
+ while (Path.has_parent_path())
+ {
+ auto ParentPath = Path.parent_path();
+ if (ParentPath == Path)
+ {
+ break;
+ }
+ if (std::filesystem::is_regular_file(ParentPath / "xmake.lua", ErrorCode))
+ {
+ if (ErrorCode)
+ {
+ break;
+ }
+
+ auto HtmlDir = ParentPath / "zenserver" / "frontend" / "html";
+ if (std::filesystem::is_directory(HtmlDir, ErrorCode))
+ {
+ m_Directory = HtmlDir;
+ }
+ break;
+ }
+ Path = ParentPath;
+ };
+#endif
+}
+
+////////////////////////////////////////////////////////////////////////////////
+HttpFrontendService::~HttpFrontendService()
+{
+}
+
+////////////////////////////////////////////////////////////////////////////////
+const char*
+HttpFrontendService::BaseUri() const
+{
+ return "/dashboard"; // in order to use the root path we need to remove HttpAddUrlToUrlGroup in HttpSys.cpp
+}
+
+////////////////////////////////////////////////////////////////////////////////
+void
+HttpFrontendService::HandleRequest(zen::HttpServerRequest& Request)
+{
+ using namespace std::literals;
+
+ std::string_view Uri = Request.RelativeUriWithExtension();
+ for (; Uri[0] == '/'; Uri = Uri.substr(1))
+ ;
+ if (Uri.empty())
+ {
+ Uri = "index.html"sv;
+ }
+
+ // Dismiss if the URI contains .. anywhere to prevent arbitrary file reads
+ if (Uri.find("..") != Uri.npos)
+ {
+ return Request.WriteResponse(HttpResponseCode::Forbidden);
+ }
+
+ // Map the file extension to a MIME type. To keep things constrained, only a
+ // small subset of file extensions is allowed
+
+ HttpContentType ContentType = HttpContentType::kUnknownContentType;
+
+ if (const size_t DotIndex = Uri.rfind("."); DotIndex != Uri.npos)
+ {
+ const std::string_view DotExt = Uri.substr(DotIndex + 1);
+
+ ContentType = ParseContentType(DotExt);
+ }
+
+ if (ContentType == HttpContentType::kUnknownContentType)
+ {
+ return Request.WriteResponse(HttpResponseCode::Forbidden);
+ }
+
+ // The given content directory overrides any zip-fs discovered in the binary
+ if (!m_Directory.empty())
+ {
+ FileContents File = ReadFile(m_Directory / Uri);
+
+ if (!File.ErrorCode)
+ {
+ return Request.WriteResponse(HttpResponseCode::OK, ContentType, File.Data[0]);
+ }
+ }
+
+ if (IoBuffer FileBuffer = m_ZipFs.GetFile(Uri))
+ {
+ return Request.WriteResponse(HttpResponseCode::OK, ContentType, FileBuffer);
+ }
+
+ Request.WriteResponse(HttpResponseCode::NotFound, HttpContentType::kText, "Not found"sv);
+}
+
+} // namespace zen
diff --git a/src/zenserver/frontend/frontend.h b/src/zenserver/frontend/frontend.h
new file mode 100644
index 000000000..6eac20620
--- /dev/null
+++ b/src/zenserver/frontend/frontend.h
@@ -0,0 +1,25 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#pragma once
+
+#include <zenhttp/httpserver.h>
+#include "zipfs.h"
+
+#include <filesystem>
+
+namespace zen {
+
+class HttpFrontendService final : public zen::HttpService
+{
+public:
+ HttpFrontendService(std::filesystem::path Directory);
+ virtual ~HttpFrontendService();
+ virtual const char* BaseUri() const override;
+ virtual void HandleRequest(zen::HttpServerRequest& Request) override;
+
+private:
+ ZipFs m_ZipFs;
+ std::filesystem::path m_Directory;
+};
+
+} // namespace zen
diff --git a/src/zenserver/frontend/html/index.html b/src/zenserver/frontend/html/index.html
new file mode 100644
index 000000000..252ee621e
--- /dev/null
+++ b/src/zenserver/frontend/html/index.html
@@ -0,0 +1,59 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" integrity="sha384-F3w7mX95PdgyTmZZMECAngseQB83DfGTowi0iMjiWaeVhAn4FJkqJByhZMI3AhiU" crossorigin="anonymous">
+ <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.min.js" integrity="sha384-skAcpIdS7UcVUC05LJ9Dxay8AXcDYfBJqt1CJ85S/CFujBsIzCIv+l9liuYLaMQ/" crossorigin="anonymous"></script>
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/font/bootstrap-icons.css">
+ <style type="text/css">
+ body {
+ background-color: #fafafa;
+ }
+ </style>
+ <script type="text/javascript">
+ const getCacheStats = () => {
+ const opts = { headers: { "Accept": "application/json" } };
+ fetch("/stats/z$", opts)
+ .then(response => {
+ if (!response.ok) {
+ throw Error(response.statusText);
+ }
+ return response.json();
+ })
+ .then(json => {
+ document.getElementById("status").innerHTML = "connected"
+ document.getElementById("stats").innerHTML = JSON.stringify(json, null, 4);
+ })
+ .catch(error => {
+ document.getElementById("status").innerHTML = "disconnected"
+ document.getElementById("stats").innerHTML = ""
+ console.log(error);
+ })
+ .finally(() => {
+ window.setTimeout(getCacheStats, 1000);
+ });
+ };
+ getCacheStats();
+ </script>
+</head>
+<body>
+ <div class="container">
+ <div class="row">
+ <div class="text-center mt-5">
+ <pre>
+__________ _________ __
+\____ / ____ ____ / _____/_/ |_ ____ _______ ____
+ / / _/ __ \ / \ \_____ \ \ __\ / _ \ \_ __ \_/ __ \
+ / /_ \ ___/ | | \ / \ | | ( <_> ) | | \/\ ___/
+/_______ \ \___ >|___| //_______ / |__| \____/ |__| \___ >
+ \/ \/ \/ \/ \/
+ </pre>
+ <pre id="status"/>
+ </div>
+ </div>
+ <div class="row">
+ <pre class="mb-0">Z$:</pre>
+ <pre id="stats"></pre>
+ <div>
+ </div>
+</body>
+</html>
diff --git a/src/zenserver/frontend/zipfs.cpp b/src/zenserver/frontend/zipfs.cpp
new file mode 100644
index 000000000..f9c2bc8ff
--- /dev/null
+++ b/src/zenserver/frontend/zipfs.cpp
@@ -0,0 +1,169 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#include "zipfs.h"
+
+namespace zen {
+
+//////////////////////////////////////////////////////////////////////////
+namespace {
+
+#if ZEN_COMPILER_MSC
+# pragma warning(push)
+# pragma warning(disable : 4200)
+#endif
+
+ using ZipInt16 = uint16_t;
+
+ struct ZipInt32
+ {
+ operator uint32_t() const { return *(uint32_t*)Parts; }
+ uint16_t Parts[2];
+ };
+
+ struct EocdRecord
+ {
+ enum : uint32_t
+ {
+ Magic = 0x0605'4b50,
+ };
+ ZipInt32 Signature;
+ ZipInt16 ThisDiskIndex;
+ ZipInt16 CdStartDiskIndex;
+ ZipInt16 CdRecordThisDiskCount;
+ ZipInt16 CdRecordCount;
+ ZipInt32 CdSize;
+ ZipInt32 CdOffset;
+ ZipInt16 CommentSize;
+ char Comment[];
+ };
+
+ struct CentralDirectoryRecord
+ {
+ enum : uint32_t
+ {
+ Magic = 0x0201'4b50,
+ };
+
+ ZipInt32 Signature;
+ ZipInt16 VersionMadeBy;
+ ZipInt16 VersionRequired;
+ ZipInt16 Flags;
+ ZipInt16 CompressionMethod;
+ ZipInt16 LastModTime;
+ ZipInt16 LastModDate;
+ ZipInt32 Crc32;
+ ZipInt32 CompressedSize;
+ ZipInt32 OriginalSize;
+ ZipInt16 FileNameLength;
+ ZipInt16 ExtraFieldLength;
+ ZipInt16 CommentLength;
+ ZipInt16 DiskIndex;
+ ZipInt16 InternalFileAttr;
+ ZipInt32 ExternalFileAttr;
+ ZipInt32 Offset;
+ char FileName[];
+ };
+
+ struct LocalFileHeader
+ {
+ enum : uint32_t
+ {
+ Magic = 0x0403'4b50,
+ };
+
+ ZipInt32 Signature;
+ ZipInt16 VersionRequired;
+ ZipInt16 Flags;
+ ZipInt16 CompressionMethod;
+ ZipInt16 LastModTime;
+ ZipInt16 LastModDate;
+ ZipInt32 Crc32;
+ ZipInt32 CompressedSize;
+ ZipInt32 OriginalSize;
+ ZipInt16 FileNameLength;
+ ZipInt16 ExtraFieldLength;
+ char FileName[];
+ };
+
+#if ZEN_COMPILER_MSC
+# pragma warning(pop)
+#endif
+
+} // namespace
+
+//////////////////////////////////////////////////////////////////////////
+ZipFs::ZipFs(IoBuffer&& Buffer)
+{
+ MemoryView View = Buffer.GetView();
+
+ uint8_t* Cursor = (uint8_t*)(View.GetData()) + View.GetSize();
+ if (View.GetSize() < sizeof(EocdRecord))
+ {
+ return;
+ }
+
+ const auto* EocdCursor = (EocdRecord*)(Cursor - sizeof(EocdRecord));
+
+ // It is more correct to search backwards for EocdRecord::Magic as the
+ // comment can be of a variable length. But here we're not going to support
+ // zip files with comments.
+ if (EocdCursor->Signature != EocdRecord::Magic)
+ {
+ return;
+ }
+
+ // Zip64 isn't supported either
+ if (EocdCursor->ThisDiskIndex == 0xffff)
+ {
+ return;
+ }
+
+ Cursor = (uint8_t*)EocdCursor - uint32_t(EocdCursor->CdOffset) - uint32_t(EocdCursor->CdSize);
+
+ const auto* CdCursor = (CentralDirectoryRecord*)(Cursor + EocdCursor->CdOffset);
+ for (int i = 0, n = EocdCursor->CdRecordCount; i < n; ++i)
+ {
+ const CentralDirectoryRecord& Cd = *CdCursor;
+
+ bool Acceptable = true;
+ Acceptable &= (Cd.OriginalSize > 0); // has some content
+ Acceptable &= (Cd.CompressionMethod == 0); // is stored uncomrpessed
+ if (Acceptable)
+ {
+ const uint8_t* Lfh = Cursor + Cd.Offset;
+ if (uintptr_t(Lfh - Cursor) < View.GetSize())
+ {
+ std::string_view FileName(Cd.FileName, Cd.FileNameLength);
+ m_Files.insert(std::make_pair(FileName, FileItem{Lfh, size_t(0)}));
+ }
+ }
+
+ uint32_t ExtraBytes = Cd.FileNameLength + Cd.ExtraFieldLength + Cd.CommentLength;
+ CdCursor = (CentralDirectoryRecord*)(Cd.FileName + ExtraBytes);
+ }
+
+ m_Buffer = std::move(Buffer);
+}
+
+//////////////////////////////////////////////////////////////////////////
+IoBuffer
+ZipFs::GetFile(const std::string_view& FileName) const
+{
+ FileMap::iterator Iter = m_Files.find(FileName);
+ if (Iter == m_Files.end())
+ {
+ return {};
+ }
+
+ FileItem& Item = Iter->second;
+ if (Item.GetSize() > 0)
+ {
+ return IoBuffer(IoBuffer::Wrap, Item.GetData(), Item.GetSize());
+ }
+
+ const auto* Lfh = (LocalFileHeader*)(Item.GetData());
+ Item = MemoryView(Lfh->FileName + Lfh->FileNameLength + Lfh->ExtraFieldLength, Lfh->OriginalSize);
+ return IoBuffer(IoBuffer::Wrap, Item.GetData(), Item.GetSize());
+}
+
+} // namespace zen
diff --git a/src/zenserver/frontend/zipfs.h b/src/zenserver/frontend/zipfs.h
new file mode 100644
index 000000000..e1fa4457c
--- /dev/null
+++ b/src/zenserver/frontend/zipfs.h
@@ -0,0 +1,26 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#pragma once
+
+#include <zencore/iobuffer.h>
+
+#include <unordered_map>
+
+namespace zen {
+
+//////////////////////////////////////////////////////////////////////////
+class ZipFs
+{
+public:
+ ZipFs() = default;
+ ZipFs(IoBuffer&& Buffer);
+ IoBuffer GetFile(const std::string_view& FileName) const;
+
+private:
+ using FileItem = MemoryView;
+ using FileMap = std::unordered_map<std::string_view, FileItem>;
+ FileMap mutable m_Files;
+ IoBuffer m_Buffer;
+};
+
+} // namespace zen