// Copyright Epic Games, Inc. All Rights Reserved. #include "frontend.h" #include #include #include #include #include #include #include ZEN_THIRD_PARTY_INCLUDES_START #if ZEN_PLATFORM_WINDOWS # include #endif ZEN_THIRD_PARTY_INCLUDES_END #if !defined(ZEN_EMBED_HTML_ZIP) # define ZEN_EMBED_HTML_ZIP ZEN_BUILD_RELEASE #endif #if ZEN_EMBED_HTML_ZIP static unsigned char gHtmlZipData[] = { # include }; #endif namespace zen { //////////////////////////////////////////////////////////////////////////////// HttpFrontendService::HttpFrontendService(std::filesystem::path Directory, HttpStatusService& StatusService) : m_Directory(Directory) , m_StatusService(StatusService) { ZEN_TRACE_CPU("HttpFrontendService::HttpFrontendService"); std::filesystem::path SelfPath = GetRunningExecutablePath(); #if ZEN_EMBED_HTML_ZIP // Load an embedded Zip archive IoBuffer HtmlZipDataBuffer(IoBuffer::Wrap, gHtmlZipData, sizeof(gHtmlZipData) - 1); m_ZipFs = std::make_unique(std::move(HtmlZipDataBuffer)); #endif if (m_Directory.empty() && !m_ZipFs) { // Probe for development layout std::error_code ErrorCode; std::filesystem::path Path = SelfPath; while (Path.has_parent_path()) { std::filesystem::path ParentPath = Path.parent_path(); if (ParentPath == Path) { break; } if (IsFile(ParentPath / "xmake.lua", ErrorCode)) { if (ErrorCode) { break; } std::filesystem::path HtmlDir = ParentPath / "src" / "zenserver" / "frontend" / "html"; if (IsDir(HtmlDir, ErrorCode)) { m_Directory = HtmlDir; } // Map data/ requests to the project docs/ directory in dev mode std::filesystem::path DocsDir = ParentPath / "docs"; if (IsDir(DocsDir, ErrorCode)) { m_DocsDirectory = DocsDir; } break; } Path = ParentPath; } } if (m_ZipFs) { ZEN_INFO("front-end is served from embedded zip"); } else if (!m_Directory.empty()) { ZEN_INFO("front-end is served from '{}'", m_Directory); } else { ZEN_INFO("front-end is NOT AVAILABLE"); } m_StatusService.RegisterHandler("dashboard", *this); } HttpFrontendService::~HttpFrontendService() { m_StatusService.UnregisterHandler("dashboard", *this); } const char* HttpFrontendService::BaseUri() const { return "/dashboard/"; // in order to use the root path we need to remove HttpAddUrlToUrlGroup in HttpSys.cpp } //////////////////////////////////////////////////////////////////////////////// void HttpFrontendService::HandleStatusRequest(zen::HttpServerRequest& Request) { CbObjectWriter Cbo; Cbo << "ok" << true; Request.WriteResponse(HttpResponseCode::OK, Cbo.Save()); } void HttpFrontendService::HandleRequest(zen::HttpServerRequest& Request) { using namespace std::literals; ExtendableStringBuilder<256> UriBuilder; std::string_view Uri = Request.RelativeUriWithExtension(); for (; Uri.length() > 0 && Uri[0] == '/'; Uri = Uri.substr(1)) ; if (Uri.empty()) { Uri = "index.html"sv; } else if (Uri.back() == '/') { UriBuilder << Uri << "index.html"sv; Uri = UriBuilder; } // 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); // Extensions used only for static file serving — not in the global // ParseContentType table because that table also drives URI extension // stripping for content negotiation, and we don't want /api/foo.txt to // have its extension removed. if (ContentType == HttpContentType::kUnknownContentType) { if (DotExt == "txt" || DotExt == "md") { ContentType = HttpContentType::kText; } } } if (ContentType == HttpContentType::kUnknownContentType) { return Request.WriteResponse(HttpResponseCode::Forbidden); } auto WriteResponseForUri = [this, &Request](std::string_view InUri, HttpResponseCode ResponseCode, HttpContentType ContentType) -> bool { // In dev mode, map data/ requests to the project docs/ directory constexpr std::string_view DataPrefix = "data/"; if (!m_DocsDirectory.empty() && InUri.starts_with(DataPrefix)) { std::string_view DocsRelative = InUri.substr(DataPrefix.size()); auto FullPath = m_DocsDirectory / std::filesystem::path(DocsRelative).make_preferred(); FileContents File = ReadFile(FullPath); if (!File.ErrorCode) { Request.WriteResponse(ResponseCode, ContentType, File.Data[0]); return true; } } // The given content directory overrides any zip-fs discovered in the binary if (!m_Directory.empty()) { auto FullPath = m_Directory / std::filesystem::path(InUri).make_preferred(); FileContents File = ReadFile(FullPath); if (!File.ErrorCode) { Request.WriteResponse(ResponseCode, ContentType, File.Data[0]); return true; } } if (m_ZipFs) { if (IoBuffer FileBuffer = m_ZipFs->GetFile(InUri)) { Request.WriteResponse(HttpResponseCode::OK, ContentType, FileBuffer); return true; } } return false; }; if (WriteResponseForUri(Uri, HttpResponseCode::OK, ContentType)) { return; } else if (WriteResponseForUri("404.html"sv, HttpResponseCode::NotFound, HttpContentType::kHTML)) { return; } else { Request.WriteResponse(HttpResponseCode::NotFound, HttpContentType::kText, "Not found"sv); } } } // namespace zen