1
0
mirror of https://github.com/ohwgiles/laminar.git synced 2025-06-13 12:54:29 +00:00

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.
This commit is contained in:
Oliver Giles 2020-03-07 20:54:52 +02:00
parent 308ce7e5d0
commit ff35071a98
7 changed files with 92 additions and 71 deletions

View File

@ -616,12 +616,6 @@ DESCRIPTION=Anything here will appear on the job page in the frontend <em>unesca
Change `LAMINAR_TITLE` in `/etc/laminar.conf` to your preferred page title. Laminar must be restarted for this change to take effect. 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 ## 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). 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).

View File

@ -211,16 +211,14 @@ kj::Promise<void> Http::request(kj::HttpMethod method, kj::StringPtr url, const
return writeEvents(p,s); return writeEvents(p,s);
}).attach(kj::mv(stream)).attach(kj::mv(peer)); }).attach(kj::mv(stream)).attach(kj::mv(peer));
} }
} } else if(url.startsWith("/archive/")) {
if(url.startsWith("/archive/")) {
KJ_IF_MAYBE(file, laminar.getArtefact(url.slice(strlen("/archive/")))) { KJ_IF_MAYBE(file, laminar.getArtefact(url.slice(strlen("/archive/")))) {
auto array = (*file)->mmap(0, (*file)->stat().size); auto array = (*file)->mmap(0, (*file)->stat().size);
responseHeaders.add("Content-Transfer-Encoding", "binary"); responseHeaders.add("Content-Transfer-Encoding", "binary");
auto stream = response.send(200, "OK", responseHeaders, array.size()); 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)); return stream->write(array.begin(), array.size()).attach(kj::mv(array)).attach(kj::mv(file)).attach(kj::mv(stream));
} }
} } else if(parseLogEndpoint(url, name, num)) {
if(parseLogEndpoint(url, name, num)) {
auto lw = kj::heap<WithSetRef<LogWatcher>>(logWatchers); auto lw = kj::heap<WithSetRef<LogWatcher>>(logWatchers);
lw->job = name; lw->job = name;
lw->run = num; lw->run = num;
@ -238,33 +236,19 @@ kj::Promise<void> Http::request(kj::HttpMethod method, kj::StringPtr url, const
return writeLogChunk(c, s); return writeLogChunk(c, s);
}).attach(kj::mv(output)).attach(kj::mv(stream)).attach(kj::mv(lw)); }).attach(kj::mv(output)).attach(kj::mv(stream)).attach(kj::mv(lw));
} }
} } else if(url == "/custom/style.css") {
if(url == "/custom/style.css") {
responseHeaders.set(kj::HttpHeaderId::CONTENT_TYPE, "text/css; charset=utf-8"); responseHeaders.set(kj::HttpHeaderId::CONTENT_TYPE, "text/css; charset=utf-8");
responseHeaders.add("Content-Transfer-Encoding", "binary"); responseHeaders.add("Content-Transfer-Encoding", "binary");
std::string css = laminar.getCustomCss(); std::string css = laminar.getCustomCss();
auto stream = response.send(200, "OK", responseHeaders, css.size()); auto stream = response.send(200, "OK", responseHeaders, css.size());
return stream->write(css.data(), css.size()).attach(kj::mv(css)).attach(kj::mv(stream)); return stream->write(css.data(), css.size()).attach(kj::mv(css)).attach(kj::mv(stream));
} } else if(resources->handleRequest(url.cStr(), &start, &end, &content_type)) {
// 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)) {
responseHeaders.set(kj::HttpHeaderId::CONTENT_TYPE, content_type); responseHeaders.set(kj::HttpHeaderId::CONTENT_TYPE, content_type);
responseHeaders.add("Content-Encoding", "gzip"); responseHeaders.add("Content-Encoding", "gzip");
responseHeaders.add("Content-Transfer-Encoding", "binary"); responseHeaders.add("Content-Transfer-Encoding", "binary");
auto stream = response.send(200, "OK", responseHeaders, end-start); auto stream = response.send(200, "OK", responseHeaders, end-start);
return stream->write(start, end-start).attach(kj::mv(stream)); return stream->write(start, end-start).attach(kj::mv(stream));
} } else if(url.startsWith("/badge/") && url.endsWith(".svg") && laminar.handleBadgeRequest(std::string(url.begin()+7, url.size()-11), badge)) {
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.set(kj::HttpHeaderId::CONTENT_TYPE, "image/svg+xml");
responseHeaders.add("Cache-Control", "no-cache"); responseHeaders.add("Cache-Control", "no-cache");
auto stream = response.send(200, "OK", responseHeaders, badge.size()); 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);
}

View File

@ -43,6 +43,9 @@ public:
void notifyEvent(const char* data, std::string job = nullptr); void notifyEvent(const char* data, std::string job = nullptr);
void notifyLog(std::string job, uint run, std::string log_chunk, bool eot); 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: private:
virtual kj::Promise<void> request(kj::HttpMethod method, kj::StringPtr url, const kj::HttpHeaders& headers, virtual kj::Promise<void> request(kj::HttpMethod method, kj::StringPtr url, const kj::HttpHeaders& headers,
kj::AsyncInputStream& requestBody, Response& response) override; kj::AsyncInputStream& requestBody, Response& response) override;

View File

@ -113,6 +113,12 @@ Laminar::Laminar(Server &server, Settings settings) :
.addPath((homePath/"cfg"/"jobs").toString(true).cStr()) .addPath((homePath/"cfg"/"jobs").toString(true).cStr())
.addPath((homePath/"cfg").toString(true).cStr()); // for groups.conf .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.listenRpc(*rpc, settings.bind_rpc);
srv.listenHttp(*http, settings.bind_http); srv.listenHttp(*http, settings.bind_http);
@ -121,6 +127,14 @@ Laminar::Laminar(Server &server, Settings settings) :
loadConfiguration(); 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) { uint Laminar::latestRun(std::string job) {
auto it = activeJobs.byJobName().equal_range(job); auto it = activeJobs.byJobName().equal_range(job);
if(it.first == it.second) { if(it.first == it.second) {
@ -789,18 +803,16 @@ R"x(
return true; return true;
} }
// TODO: deprecate
std::string Laminar::getCustomCss() { std::string Laminar::getCustomCss() {
KJ_IF_MAYBE(cssFile, fsHome->tryOpenFile(kj::Path{"custom","style.css"})) { 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(); return (*cssFile)->readAllText().cStr();
} else { } else {
return std::string(); 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();
}
}

View File

@ -95,10 +95,6 @@ public:
// which handles this url. // which handles this url.
std::string getCustomCss(); 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 // Aborts a single job
bool abort(std::string job, uint buildNum); bool abort(std::string job, uint buildNum);
@ -107,6 +103,7 @@ public:
private: private:
bool loadConfiguration(); bool loadConfiguration();
void loadCustomizations();
void assignNewJobs(); void assignNewJobs();
bool tryStartRun(std::shared_ptr<Run> run, int queueIndex); bool tryStartRun(std::shared_ptr<Run> run, int queueIndex);
void handleRunFinished(Run*); void handleRunFinished(Run*);

View File

@ -37,7 +37,6 @@
Resources::Resources() Resources::Resources()
{ {
INIT_RESOURCE("/", index_html, CONTENT_TYPE_HTML);
INIT_RESOURCE("/favicon.ico", favicon_ico, CONTENT_TYPE_ICO); INIT_RESOURCE("/favicon.ico", favicon_ico, CONTENT_TYPE_ICO);
INIT_RESOURCE("/favicon-152.png", favicon_152_png, CONTENT_TYPE_PNG); INIT_RESOURCE("/favicon-152.png", favicon_152_png, CONTENT_TYPE_PNG);
INIT_RESOURCE("/icon.png", icon_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/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("/js/Chart.min.js", js_Chart_min_js, CONTENT_TYPE_JS);
INIT_RESOURCE("/css/bootstrap.min.css", css_bootstrap_min_css, CONTENT_TYPE_CSS); 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")) { void Resources::setHtmlTemplate(std::string tmpl) {
// The administrator needs to customize the <base href>. Unfortunately this seems extern const char _binary_index_html_z_start[];
// to be the only thing that needs to be customizable but cannot be done via dynamic extern const char _binary_index_html_z_end[];
// DOM manipulation without heavy compromises. So replace the static char array with
// a modified buffer accordingly. z_stream strm;
z_stream strm; memset(&strm, 0, sizeof(z_stream));
memset(&strm, 0, sizeof(z_stream));
std::string tmp; if(!tmpl.empty()) {
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 // 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); deflateInit2(&strm, Z_DEFAULT_COMPRESSION, Z_DEFLATED, MAX_WBITS|GZIP_FORMAT, 8, Z_DEFAULT_STRATEGY);
strm.next_in = (unsigned char*) tmp.data(); strm.next_in = (unsigned char*) tmpl.data();
strm.avail_in = tmp.size(); strm.avail_in = tmpl.size();
strm.next_out = (unsigned char*) index_html.data(); 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) { if(deflate(&strm, Z_FINISH) != Z_STREAM_END) {
LLOG(FATAL, "Failed to compress index.html"); LLOG(FATAL, "Failed to compress index.html");
} }
index_html.resize(strm.total_out); index_html.resize(strm.total_out);
// update resource map } else {
resources["/"].start = index_html.data(); // use the default template from compile-time asset
resources["/"].end = index_html.data() + index_html.size(); if(const char* baseUrl = getenv("LAMINAR_BASE_URL")) {
// The administrator needs to customize the <base href>. 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) { inline bool beginsWith(std::string haystack, const char* needle) {

View File

@ -34,6 +34,9 @@ public:
// type. Function returns false if no resource for the given path exists // 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); 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: private:
struct Resource { struct Resource {
const char* start; const char* start;