mirror of
https://github.com/ohwgiles/laminar.git
synced 2024-10-27 20:34:20 +00:00
resolves #80: reverse-proxy with custom base URL
Fix all hrefs and vue routes to correctly operate against the <base href> tag. Add a configuration parameter to override the content of the href attribute, and describe its use.
This commit is contained in:
parent
210787a352
commit
95482c78a5
@ -1,5 +1,5 @@
|
|||||||
###
|
###
|
||||||
### Copyright 2015-2018 Oliver Giles
|
### Copyright 2015-2019 Oliver Giles
|
||||||
###
|
###
|
||||||
### This file is part of Laminar
|
### This file is part of Laminar
|
||||||
###
|
###
|
||||||
@ -64,6 +64,11 @@ add_custom_command(OUTPUT laminar.capnp.c++ laminar.capnp.h
|
|||||||
generate_compressed_bins(${CMAKE_SOURCE_DIR}/src/resources index.html js/app.js
|
generate_compressed_bins(${CMAKE_SOURCE_DIR}/src/resources index.html js/app.js
|
||||||
favicon.ico favicon-152.png icon.png)
|
favicon.ico favicon-152.png icon.png)
|
||||||
|
|
||||||
|
# The code that allows dynamic modifying of index.html requires knowing its original size
|
||||||
|
add_custom_command(OUTPUT index_html_size.h
|
||||||
|
COMMAND sh -c '( echo -n "\\#define INDEX_HTML_UNCOMPRESSED_SIZE " && wc -c < "${CMAKE_SOURCE_DIR}/src/resources/index.html" ) > index_html_size.h'
|
||||||
|
DEPENDS src/resources/index.html)
|
||||||
|
|
||||||
# Download 3rd-party frontend JS libs...
|
# Download 3rd-party frontend JS libs...
|
||||||
file(DOWNLOAD https://cdnjs.cloudflare.com/ajax/libs/vue/2.3.4/vue.min.js
|
file(DOWNLOAD https://cdnjs.cloudflare.com/ajax/libs/vue/2.3.4/vue.min.js
|
||||||
js/vue.min.js EXPECTED_MD5 ae2fca1cfa0e31377819b1b0ffef704c)
|
js/vue.min.js EXPECTED_MD5 ae2fca1cfa0e31377819b1b0ffef704c)
|
||||||
@ -82,7 +87,7 @@ generate_compressed_bins(${CMAKE_BINARY_DIR} js/vue-router.min.js js/vue.min.js
|
|||||||
|
|
||||||
## Server
|
## Server
|
||||||
add_executable(laminard src/database.cpp src/main.cpp src/server.cpp src/laminar.cpp
|
add_executable(laminard src/database.cpp src/main.cpp src/server.cpp src/laminar.cpp
|
||||||
src/conf.cpp src/resources.cpp src/run.cpp laminar.capnp.c++ ${COMPRESSED_BINS})
|
src/conf.cpp src/resources.cpp src/run.cpp laminar.capnp.c++ ${COMPRESSED_BINS} index_html_size.h)
|
||||||
target_link_libraries(laminard capnp-rpc capnp kj-http kj-async kj pthread sqlite3 z)
|
target_link_libraries(laminard capnp-rpc capnp kj-http kj-async kj pthread sqlite3 z)
|
||||||
|
|
||||||
## Client
|
## Client
|
||||||
|
@ -69,6 +69,8 @@ For Apache, see [Apache Reverse Proxy](https://httpd.apache.org/docs/2.4/howto/r
|
|||||||
|
|
||||||
If you use [artefacts](#Archiving-artefacts), note that Laminar is not designed as a file server, and better performance will be achieved by allowing the frontend web server to directly serve the archive directory directly (e.g. using a `Location` directive).
|
If you use [artefacts](#Archiving-artefacts), note that Laminar is not designed as a file server, and better performance will be achieved by allowing the frontend web server to directly serve the archive directory directly (e.g. using a `Location` directive).
|
||||||
|
|
||||||
|
If you use a reverse proxy to host Laminar at a subfolder instead of a subdomain root, the `<base href>` needs to be updated to ensure all links point to their proper targets. This can be done by setting `LAMINAR_BASE_URL` in `/etc/laminar.conf`.
|
||||||
|
|
||||||
## Set the page title
|
## Set the page title
|
||||||
|
|
||||||
Change `LAMINAR_TITLE` in `/etc/laminar.conf` to your preferred page title. For further WebUI customization, consider using a [custom style sheet](#Customizing-the-WebUI).
|
Change `LAMINAR_TITLE` in `/etc/laminar.conf` to your preferred page title. For further WebUI customization, consider using a [custom style sheet](#Customizing-the-WebUI).
|
||||||
|
@ -43,6 +43,16 @@
|
|||||||
###
|
###
|
||||||
#LAMINAR_KEEP_RUNDIRS=0
|
#LAMINAR_KEEP_RUNDIRS=0
|
||||||
|
|
||||||
|
|
||||||
|
###
|
||||||
|
### LAMINAR_BASE_URL
|
||||||
|
###
|
||||||
|
### Base url for the frontend. This affects the <base href> tag and needs
|
||||||
|
### to be set if Laminar runs behind a reverse-proxy that hosts Laminar
|
||||||
|
### within a subfolder (rather than at a subdomain root)
|
||||||
|
###
|
||||||
|
#LAMINAR_BASE_URL=/
|
||||||
|
|
||||||
###
|
###
|
||||||
### LAMINAR_ARCHIVE_URL
|
### LAMINAR_ARCHIVE_URL
|
||||||
###
|
###
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
///
|
///
|
||||||
/// Copyright 2015-2017 Oliver Giles
|
/// Copyright 2015-2019 Oliver Giles
|
||||||
///
|
///
|
||||||
/// This file is part of Laminar
|
/// This file is part of Laminar
|
||||||
///
|
///
|
||||||
@ -17,7 +17,10 @@
|
|||||||
/// along with Laminar. If not, see <http://www.gnu.org/licenses/>
|
/// along with Laminar. If not, see <http://www.gnu.org/licenses/>
|
||||||
///
|
///
|
||||||
#include "resources.h"
|
#include "resources.h"
|
||||||
|
#include "log.h"
|
||||||
|
#include "index_html_size.h"
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
#include <zlib.h>
|
||||||
|
|
||||||
#define INIT_RESOURCE(route, name, content_type) \
|
#define INIT_RESOURCE(route, name, content_type) \
|
||||||
extern const char _binary_##name##_z_start[];\
|
extern const char _binary_##name##_z_start[];\
|
||||||
@ -30,6 +33,8 @@
|
|||||||
#define CONTENT_TYPE_JS "application/javascript; charset=utf-8"
|
#define CONTENT_TYPE_JS "application/javascript; charset=utf-8"
|
||||||
#define CONTENT_TYPE_CSS "text/css; charset=utf-8"
|
#define CONTENT_TYPE_CSS "text/css; charset=utf-8"
|
||||||
|
|
||||||
|
#define GZIP_FORMAT 16
|
||||||
|
|
||||||
Resources::Resources()
|
Resources::Resources()
|
||||||
{
|
{
|
||||||
INIT_RESOURCE("/", index_html, CONTENT_TYPE_HTML);
|
INIT_RESOURCE("/", index_html, CONTENT_TYPE_HTML);
|
||||||
@ -43,6 +48,46 @@ 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);
|
||||||
|
|
||||||
|
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.
|
||||||
|
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);
|
||||||
|
// 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);
|
||||||
|
// update resource map
|
||||||
|
resources["/"].start = index_html.data();
|
||||||
|
resources["/"].end = index_html.data() + index_html.size();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inline bool beginsWith(std::string haystack, const char* needle) {
|
inline bool beginsWith(std::string haystack, const char* needle) {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
///
|
///
|
||||||
/// Copyright 2015-2017 Oliver Giles
|
/// Copyright 2015-2019 Oliver Giles
|
||||||
///
|
///
|
||||||
/// This file is part of Laminar
|
/// This file is part of Laminar
|
||||||
///
|
///
|
||||||
@ -40,7 +40,8 @@ private:
|
|||||||
const char* end;
|
const char* end;
|
||||||
const char* content_type;
|
const char* content_type;
|
||||||
};
|
};
|
||||||
std::unordered_map<std::string, const Resource> resources;
|
std::unordered_map<std::string, Resource> resources;
|
||||||
|
std::string index_html;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // LAMINAR_RESOURCES_H_
|
#endif // LAMINAR_RESOURCES_H_
|
||||||
|
@ -6,15 +6,16 @@
|
|||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
<link rel="apple-touch-icon-precomposed" href="/favicon-152.png">
|
<link rel="apple-touch-icon-precomposed" href="favicon-152.png">
|
||||||
|
<link rel="icon" href="favicon.ico">
|
||||||
<title>Laminar</title>
|
<title>Laminar</title>
|
||||||
<script src="/js/vue.min.js"></script>
|
<script src="js/vue.min.js"></script>
|
||||||
<script src="/js/vue-router.min.js"></script>
|
<script src="js/vue-router.min.js"></script>
|
||||||
<script src="/js/ansi_up.js"></script>
|
<script src="js/ansi_up.js"></script>
|
||||||
<script src="/js/Chart.min.js"></script>
|
<script src="js/Chart.min.js"></script>
|
||||||
<link href="/css/bootstrap.min.css" rel="stylesheet">
|
<link href="css/bootstrap.min.css" rel="stylesheet">
|
||||||
<link href="/custom/style.css" rel="stylesheet">
|
<link href="custom/style.css" rel="stylesheet">
|
||||||
<script src="/js/app.js" defer></script>
|
<script src="js/app.js" defer></script>
|
||||||
<style>
|
<style>
|
||||||
body, html { height: 100%; }
|
body, html { height: 100%; }
|
||||||
.navbar { margin-bottom: 0; border-radius: 0; }
|
.navbar { margin-bottom: 0; border-radius: 0; }
|
||||||
@ -326,7 +327,7 @@
|
|||||||
<div id="app">
|
<div id="app">
|
||||||
<nav class="navbar navbar-inverse">
|
<nav class="navbar navbar-inverse">
|
||||||
<div>
|
<div>
|
||||||
<router-link to="/" class="navbar-brand"><img src="/icon.png">{{title}}</router-link>
|
<router-link to="/" class="navbar-brand"><img src="icon.png">{{title}}</router-link>
|
||||||
<router-link to="/jobs" class="btn navbar-btn pull-right">Jobs</router-link>
|
<router-link to="/jobs" class="btn navbar-btn pull-right">Jobs</router-link>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
@ -23,13 +23,9 @@ const timeScale = function(max){
|
|||||||
: { scale:function(v){return v;}, label:'Seconds' };
|
: { scale:function(v){return v;}, label:'Seconds' };
|
||||||
}
|
}
|
||||||
|
|
||||||
const wsp = function(path) {
|
|
||||||
return new WebSocket((location.protocol === 'https:'?'wss://':'ws://')
|
|
||||||
+ location.host + path);
|
|
||||||
}
|
|
||||||
const WebsocketHandler = function() {
|
const WebsocketHandler = function() {
|
||||||
function setupWebsocket(path, next) {
|
function setupWebsocket(path, next) {
|
||||||
var ws = wsp(path);
|
let ws = new WebSocket(document.head.baseURI.replace(/^http/,'ws') + path.substr(1));
|
||||||
ws.onmessage = function(msg) {
|
ws.onmessage = function(msg) {
|
||||||
msg = JSON.parse(msg.data);
|
msg = JSON.parse(msg.data);
|
||||||
// "status" is the first message the websocket always delivers.
|
// "status" is the first message the websocket always delivers.
|
||||||
@ -668,7 +664,7 @@ const Run = function() {
|
|||||||
var firstLog = false;
|
var firstLog = false;
|
||||||
const logFetcher = (vm, name, num) => {
|
const logFetcher = (vm, name, num) => {
|
||||||
const abort = new AbortController();
|
const abort = new AbortController();
|
||||||
fetch('/log/'+name+'/'+num, {signal:abort.signal}).then(res => {
|
fetch('log/'+name+'/'+num, {signal:abort.signal}).then(res => {
|
||||||
// ATOW pipeThrough not supported in Firefox
|
// ATOW pipeThrough not supported in Firefox
|
||||||
//const reader = res.body.pipeThrough(new TextDecoderStream).getReader();
|
//const reader = res.body.pipeThrough(new TextDecoderStream).getReader();
|
||||||
const reader = res.body.getReader();
|
const reader = res.body.getReader();
|
||||||
@ -678,7 +674,7 @@ const Run = function() {
|
|||||||
value = utf8decoder.decode(value);
|
value = utf8decoder.decode(value);
|
||||||
if (done)
|
if (done)
|
||||||
return;
|
return;
|
||||||
state.log += ansi_up.ansi_to_html(value.replace(/</g,'<').replace(/>/g,'>').replace(/\033\[\{([^:]+):(\d+)\033\\/g, (m,$1,$2)=>{return '<a href="/jobs/'+$1+'" onclick="return vroute(this);">'+$1+'</a>:<a href="/jobs/'+$1+'/'+$2+'" onclick="return vroute(this);">#'+$2+'</a>';}));
|
state.log += ansi_up.ansi_to_html(value.replace(/</g,'<').replace(/>/g,'>').replace(/\033\[\{([^:]+):(\d+)\033\\/g, (m,$1,$2)=>{return '<a href="jobs/'+$1+'" onclick="return vroute(this);">'+$1+'</a>:<a href="jobs/'+$1+'/'+$2+'" onclick="return vroute(this);">#'+$2+'</a>';}));
|
||||||
vm.$forceUpdate();
|
vm.$forceUpdate();
|
||||||
if (!firstLog) {
|
if (!firstLog) {
|
||||||
firstLog = true;
|
firstLog = true;
|
||||||
@ -799,6 +795,7 @@ new Vue({
|
|||||||
},
|
},
|
||||||
router: new VueRouter({
|
router: new VueRouter({
|
||||||
mode: 'history',
|
mode: 'history',
|
||||||
|
base: document.head.baseURI.substr(location.origin.length),
|
||||||
routes: [
|
routes: [
|
||||||
{ path: '/', component: Home },
|
{ path: '/', component: Home },
|
||||||
{ path: '/jobs', component: Jobs },
|
{ path: '/jobs', component: Jobs },
|
||||||
|
Loading…
Reference in New Issue
Block a user