From ff35071a988a9931e8a7025688407bf9787636cf Mon Sep 17 00:00:00 2001 From: Oliver Giles Date: Sat, 7 Mar 2020 20:54:52 +0200 Subject: [PATCH] Improved implementation of custom HTML template This implementation watches for changes to the template via inotify instead of checking for every request. This also removes some URL parsing logic duplication because the original mechanism can be reused. Also, asset is gzipped for faster delivery. Deprecate custom css because this makes it redundant. --- UserManual.md | 6 ---- src/http.cpp | 31 ++++++----------- src/http.h | 3 ++ src/laminar.cpp | 28 ++++++++++----- src/laminar.h | 5 +-- src/resources.cpp | 87 ++++++++++++++++++++++++++++++----------------- src/resources.h | 3 ++ 7 files changed, 92 insertions(+), 71 deletions(-) diff --git a/UserManual.md b/UserManual.md index 109de8f..7509bb5 100644 --- a/UserManual.md +++ b/UserManual.md @@ -616,12 +616,6 @@ DESCRIPTION=Anything here will appear on the job page in the frontend unesca Change `LAMINAR_TITLE` in `/etc/laminar.conf` to your preferred page title. Laminar must be restarted for this change to take effect. -## Custom stylesheet - -If it exists, the file `/var/lib/laminar/custom/style.css` will be served by laminar and may be used to change the appearance of Laminar's WebUI. - -This directory is also a good place to add any extra assets needed for this customization, but note that in this case you will need to serve this directory directly from your [HTTP reverse proxy](#Service-configuration) (highly recommended). - ## Custom HTML template If it exists, the file `/var/lib/laminar/custom/index.html` will be served by laminar instead of the default markup that is bundled into the Laminar binary. This file can be used to change any aspect of Laminar's WebUI, including adding custom menu links, stylesheets, or anything else. Any additional assets that are needed will need to be served directly from your [HTTP reverse proxy](#Service-configuration) (highly recommended). diff --git a/src/http.cpp b/src/http.cpp index 7db8fd6..afb99e3 100644 --- a/src/http.cpp +++ b/src/http.cpp @@ -211,16 +211,14 @@ kj::Promise Http::request(kj::HttpMethod method, kj::StringPtr url, const return writeEvents(p,s); }).attach(kj::mv(stream)).attach(kj::mv(peer)); } - } - if(url.startsWith("/archive/")) { + } else if(url.startsWith("/archive/")) { KJ_IF_MAYBE(file, laminar.getArtefact(url.slice(strlen("/archive/")))) { auto array = (*file)->mmap(0, (*file)->stat().size); responseHeaders.add("Content-Transfer-Encoding", "binary"); auto stream = response.send(200, "OK", responseHeaders, array.size()); return stream->write(array.begin(), array.size()).attach(kj::mv(array)).attach(kj::mv(file)).attach(kj::mv(stream)); } - } - if(parseLogEndpoint(url, name, num)) { + } else if(parseLogEndpoint(url, name, num)) { auto lw = kj::heap>(logWatchers); lw->job = name; lw->run = num; @@ -238,33 +236,19 @@ kj::Promise Http::request(kj::HttpMethod method, kj::StringPtr url, const return writeLogChunk(c, s); }).attach(kj::mv(output)).attach(kj::mv(stream)).attach(kj::mv(lw)); } - } - if(url == "/custom/style.css") { + } else if(url == "/custom/style.css") { responseHeaders.set(kj::HttpHeaderId::CONTENT_TYPE, "text/css; charset=utf-8"); responseHeaders.add("Content-Transfer-Encoding", "binary"); std::string css = laminar.getCustomCss(); auto stream = response.send(200, "OK", responseHeaders, css.size()); return stream->write(css.data(), css.size()).attach(kj::mv(css)).attach(kj::mv(stream)); - } - // If there is custom html defined, serve it. Otherwise, the default html - // will be served by the next block. - if(url == "/" || url == "/index.html" || url.startsWith("/jobs")) { - responseHeaders.set(kj::HttpHeaderId::CONTENT_TYPE, "text/html; charset=utf-8"); - responseHeaders.add("Content-Transfer-Encoding", "binary"); - std::string html = laminar.getCustomIndexHtml(); - if (!html.empty()) { - auto stream = response.send(200, "OK", responseHeaders, html.size()); - return stream->write(html.data(), html.size()).attach(kj::mv(html)).attach(kj::mv(stream)); - } - } - if(resources->handleRequest(url.cStr(), &start, &end, &content_type)) { + } else if(resources->handleRequest(url.cStr(), &start, &end, &content_type)) { responseHeaders.set(kj::HttpHeaderId::CONTENT_TYPE, content_type); responseHeaders.add("Content-Encoding", "gzip"); responseHeaders.add("Content-Transfer-Encoding", "binary"); auto stream = response.send(200, "OK", responseHeaders, end-start); return stream->write(start, end-start).attach(kj::mv(stream)); - } - if(url.startsWith("/badge/") && url.endsWith(".svg") && laminar.handleBadgeRequest(std::string(url.begin()+7, url.size()-11), badge)) { + } else if(url.startsWith("/badge/") && url.endsWith(".svg") && laminar.handleBadgeRequest(std::string(url.begin()+7, url.size()-11), badge)) { responseHeaders.set(kj::HttpHeaderId::CONTENT_TYPE, "image/svg+xml"); responseHeaders.add("Cache-Control", "no-cache"); auto stream = response.send(200, "OK", responseHeaders, badge.size()); @@ -313,3 +297,8 @@ void Http::notifyLog(std::string job, uint run, std::string log_chunk, bool eot) } } } + +void Http::setHtmlTemplate(std::string tmpl) +{ + resources->setHtmlTemplate(tmpl); +} diff --git a/src/http.h b/src/http.h index 1d37755..85d6469 100644 --- a/src/http.h +++ b/src/http.h @@ -43,6 +43,9 @@ public: void notifyEvent(const char* data, std::string job = nullptr); void notifyLog(std::string job, uint run, std::string log_chunk, bool eot); + // Allows supplying a custom HTML template. Pass an empty string to use the default. + void setHtmlTemplate(std::string tmpl = std::string()); + private: virtual kj::Promise request(kj::HttpMethod method, kj::StringPtr url, const kj::HttpHeaders& headers, kj::AsyncInputStream& requestBody, Response& response) override; diff --git a/src/laminar.cpp b/src/laminar.cpp index 9a5a830..3757830 100644 --- a/src/laminar.cpp +++ b/src/laminar.cpp @@ -113,6 +113,12 @@ Laminar::Laminar(Server &server, Settings settings) : .addPath((homePath/"cfg"/"jobs").toString(true).cStr()) .addPath((homePath/"cfg").toString(true).cStr()); // for groups.conf + loadCustomizations(); + srv.watchPaths([this]{ + LLOG(INFO, "Reloading customizations"); + loadCustomizations(); + }).addPath((homePath/"custom").toString(true).cStr()); + srv.listenRpc(*rpc, settings.bind_rpc); srv.listenHttp(*http, settings.bind_http); @@ -121,6 +127,14 @@ Laminar::Laminar(Server &server, Settings settings) : loadConfiguration(); } +void Laminar::loadCustomizations() { + KJ_IF_MAYBE(templ, fsHome->tryOpenFile(kj::Path{"custom","index.html"})) { + http->setHtmlTemplate((*templ)->readAllText().cStr()); + } else { + http->setHtmlTemplate(); + } +} + uint Laminar::latestRun(std::string job) { auto it = activeJobs.byJobName().equal_range(job); if(it.first == it.second) { @@ -789,18 +803,16 @@ R"x( return true; } +// TODO: deprecate std::string Laminar::getCustomCss() { KJ_IF_MAYBE(cssFile, fsHome->tryOpenFile(kj::Path{"custom","style.css"})) { + static bool warningShown = false; + if(!warningShown) { + LLOG(WARNING, "Custom CSS has been deprecated and will be removed from a future release. Use a custom HTML template instead."); + warningShown = true; + } return (*cssFile)->readAllText().cStr(); } else { return std::string(); } } - -std::string Laminar::getCustomIndexHtml() { - KJ_IF_MAYBE(htmlFile, fsHome->tryOpenFile(kj::Path{"custom","index.html"})) { - return (*htmlFile)->readAllText().cStr(); - } else { - return std::string(); - } -} diff --git a/src/laminar.h b/src/laminar.h index 02cf2be..a47b76e 100644 --- a/src/laminar.h +++ b/src/laminar.h @@ -95,10 +95,6 @@ public: // which handles this url. std::string getCustomCss(); - // Fetches the content of $LAMINAR_HOME/custom/index.html or an empty - // string. This is used for custom template overrides. - std::string getCustomIndexHtml(); - // Aborts a single job bool abort(std::string job, uint buildNum); @@ -107,6 +103,7 @@ public: private: bool loadConfiguration(); + void loadCustomizations(); void assignNewJobs(); bool tryStartRun(std::shared_ptr run, int queueIndex); void handleRunFinished(Run*); diff --git a/src/resources.cpp b/src/resources.cpp index 60c1c2c..a8561d7 100644 --- a/src/resources.cpp +++ b/src/resources.cpp @@ -37,7 +37,6 @@ Resources::Resources() { - INIT_RESOURCE("/", index_html, CONTENT_TYPE_HTML); INIT_RESOURCE("/favicon.ico", favicon_ico, CONTENT_TYPE_ICO); INIT_RESOURCE("/favicon-152.png", favicon_152_png, CONTENT_TYPE_PNG); INIT_RESOURCE("/icon.png", icon_png, CONTENT_TYPE_PNG); @@ -48,46 +47,70 @@ Resources::Resources() INIT_RESOURCE("/js/ansi_up.js", js_ansi_up_js, CONTENT_TYPE_JS); INIT_RESOURCE("/js/Chart.min.js", js_Chart_min_js, CONTENT_TYPE_JS); INIT_RESOURCE("/css/bootstrap.min.css", css_bootstrap_min_css, CONTENT_TYPE_CSS); + // Configure the default template + setHtmlTemplate(std::string()); +} - if(const char* baseUrl = getenv("LAMINAR_BASE_URL")) { - // The administrator needs to customize the . Unfortunately this seems - // to be the only thing that needs to be customizable but cannot be done via dynamic - // DOM manipulation without heavy compromises. So replace the static char array with - // a modified buffer accordingly. - z_stream strm; - memset(&strm, 0, sizeof(z_stream)); - std::string tmp; - tmp.resize(INDEX_HTML_UNCOMPRESSED_SIZE); - // inflate - inflateInit2(&strm, MAX_WBITS|GZIP_FORMAT); - strm.next_in = (unsigned char*) _binary_index_html_z_start; - strm.avail_in = _binary_index_html_z_end - _binary_index_html_z_start; - strm.next_out = (unsigned char*) tmp.data(); - strm.avail_out = INDEX_HTML_UNCOMPRESSED_SIZE; - if(inflate(&strm, Z_FINISH) != Z_STREAM_END) { - LLOG(FATAL, "Failed to uncompress index_html"); - } - // replace - // There's no validation on the replacement string, so you can completely mangle - // the html if you like. This isn't really an issue because if you can modify laminar's - // environment you already have elevated permissions - if(auto it = tmp.find("base href=\"/")) - tmp.replace(it+11, 1, baseUrl); +void Resources::setHtmlTemplate(std::string tmpl) { + extern const char _binary_index_html_z_start[]; + extern const char _binary_index_html_z_end[]; + + z_stream strm; + memset(&strm, 0, sizeof(z_stream)); + + if(!tmpl.empty()) { // deflate - index_html.resize(tmp.size()); + index_html.resize(tmpl.size()); deflateInit2(&strm, Z_DEFAULT_COMPRESSION, Z_DEFLATED, MAX_WBITS|GZIP_FORMAT, 8, Z_DEFAULT_STRATEGY); - strm.next_in = (unsigned char*) tmp.data(); - strm.avail_in = tmp.size(); + strm.next_in = (unsigned char*) tmpl.data(); + strm.avail_in = tmpl.size(); strm.next_out = (unsigned char*) index_html.data(); - strm.avail_out = tmp.size(); + strm.avail_out = tmpl.size(); if(deflate(&strm, Z_FINISH) != Z_STREAM_END) { LLOG(FATAL, "Failed to compress index.html"); } index_html.resize(strm.total_out); - // update resource map - resources["/"].start = index_html.data(); - resources["/"].end = index_html.data() + index_html.size(); + } else { + // use the default template from compile-time asset + if(const char* baseUrl = getenv("LAMINAR_BASE_URL")) { + // The administrator needs to customize the . Unfortunately this seems + // to be the only thing that needs to be customizable but cannot be done via dynamic + // DOM manipulation without heavy compromises. So replace the static char array with + // a modified buffer accordingly. + std::string tmp; + tmp.resize(INDEX_HTML_UNCOMPRESSED_SIZE); + // inflate + inflateInit2(&strm, MAX_WBITS|GZIP_FORMAT); + strm.next_in = (unsigned char*) _binary_index_html_z_start; + strm.avail_in = _binary_index_html_z_end - _binary_index_html_z_start; + strm.next_out = (unsigned char*) tmp.data(); + strm.avail_out = INDEX_HTML_UNCOMPRESSED_SIZE; + if(inflate(&strm, Z_FINISH) != Z_STREAM_END) { + LLOG(FATAL, "Failed to uncompress index_html"); + } + // replace + // There's no validation on the replacement string, so you can completely mangle + // the html if you like. This isn't really an issue because if you can modify laminar's + // environment you already have elevated permissions + if(auto it = tmp.find("base href=\"/")) + tmp.replace(it+11, 1, baseUrl); + // deflate + index_html.resize(tmp.size()); + deflateInit2(&strm, Z_DEFAULT_COMPRESSION, Z_DEFLATED, MAX_WBITS|GZIP_FORMAT, 8, Z_DEFAULT_STRATEGY); + strm.next_in = (unsigned char*) tmp.data(); + strm.avail_in = tmp.size(); + strm.next_out = (unsigned char*) index_html.data(); + strm.avail_out = tmp.size(); + if(deflate(&strm, Z_FINISH) != Z_STREAM_END) { + LLOG(FATAL, "Failed to compress index.html"); + } + index_html.resize(strm.total_out); + } else { + index_html = std::string(_binary_index_html_z_start, _binary_index_html_z_end); + } } + // update resource map + resources["/"] = Resource{index_html.data(), index_html.data() + index_html.size(), CONTENT_TYPE_HTML}; } inline bool beginsWith(std::string haystack, const char* needle) { diff --git a/src/resources.h b/src/resources.h index 14ef6de..1f8f951 100644 --- a/src/resources.h +++ b/src/resources.h @@ -34,6 +34,9 @@ public: // type. Function returns false if no resource for the given path exists bool handleRequest(std::string path, const char** start, const char** end, const char** content_type); + // Allows providing a custom HTML template. Pass an empty string to use the default. + void setHtmlTemplate(std::string templ = std::string()); + private: struct Resource { const char* start;