diff --git a/README.md b/README.md index ae11836..b2f858d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # frontail – streaming logs to the browser -```frontail``` is a Node.js application for streaming logs to the browser. It's a `tail -F` with UI. +`frontail` is a Node.js application for streaming logs to the browser. It's a `tail -F` with UI. ![frontial](https://user-images.githubusercontent.com/455261/29570317-660c8122-8756-11e7-9d2f-8fea19e05211.gif) @@ -9,9 +9,9 @@ ## Quick start -- `npm i frontail -g` or download a binary file from [Releases](https://github.com/mthenw/frontail/releases) page -- `frontail /var/log/syslog` -- visit [http://127.0.0.1:9001](http://127.0.0.1:9001) +* `npm i frontail -g` or download a binary file from [Releases](https://github.com/mthenw/frontail/releases) page +* `frontail /var/log/syslog` +* visit [http://127.0.0.1:9001](http://127.0.0.1:9001) ## Features @@ -21,7 +21,7 @@ * number of unread logs in favicon * themes (default, dark) * [highlighting](#highlighting) -* search (```Tab``` to focus, ```Esc``` to clear) +* search (`Tab` to focus, `Esc` to clear) * tailing [multiple files](#tailing-multiple-files) and [stdin](#stdin) * basic authentication @@ -51,10 +51,12 @@ -c, --certificate Certificate for HTTPS, option works only along with -k option --pid-path if run as daemon file that will store the process id, default /var/run/frontail.pid --log-path if run as daemon file that will be used as a log, default /dev/null + --url-path URL path for the browser application, default / --ui-hide-topbar hide topbar (log file name and search box) --ui-no-indent don't indent log lines --ui-highlight highlight words or lines if defined string found in logs, default preset --ui-highlight-preset custom preset for highlighting (see ./preset/default.json) + --path prefix path for the running application, default / Web interface runs on **http://127.0.0.1:[port]**. @@ -70,7 +72,7 @@ Use `-` for streaming stdin: ### Highlighting -```--ui-highlight``` option turns on highlighting in UI. By default preset from ```./preset/default.json``` is used: +`--ui-highlight` option turns on highlighting in UI. By default preset from `./preset/default.json` is used: ``` { @@ -85,4 +87,31 @@ Use `-` for streaming stdin: which means that every "err" string will be in red and every line containing "err" will be bolded. -*New presets are very welcome. If you don't like default or you would like to share yours, please create PR with json file.* +_New presets are very welcome. If you don't like default or you would like to share yours, please create PR with json file._ + +### Running behind nginx + +Using the `--url-path` option `frontail` can run behind nginx with the example configuration + +Using `frontail` with `--url-path /frontail` + +``` +events { + worker_connections 1024; +} + +http { + server { + listen 8080; + server_name localhost; + + location /frontail { + proxy_pass http://127.0.0.1:9001/frontail; + + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } + } +} +``` diff --git a/index.js b/index.js index 1622251..8923df1 100644 --- a/index.js +++ b/index.js @@ -4,7 +4,7 @@ const connect = require('connect'); const cookieParser = require('cookie'); const crypto = require('crypto'); const path = require('path'); -const socketio = require('socket.io'); +const SocketIO = require('socket.io'); const tail = require('./lib/tail'); const connectBuilder = require('./lib/connect_builder'); const program = require('./lib/options_parser'); @@ -30,7 +30,11 @@ const doSecure = !!(program.key && program.certificate); const sessionSecret = String(+new Date()) + Math.random(); const sessionKey = 'sid'; const files = program.args.join(' '); -const filesNamespace = crypto.createHash('md5').update(files).digest('hex'); +const filesNamespace = crypto + .createHash('md5') + .update(files) + .digest('hex'); +const urlPath = program.urlPath.replace(/\/$/, ''); // remove trailing slash if (program.daemonize) { daemonize(__filename, program, { @@ -41,7 +45,7 @@ if (program.daemonize) { /** * HTTP(s) server setup */ - const appBuilder = connectBuilder(); + const appBuilder = connectBuilder(urlPath); if (doAuthorization) { appBuilder.session(sessionSecret, sessionKey); appBuilder.authorize(program.user, program.password); @@ -63,9 +67,8 @@ if (program.daemonize) { /** * socket.io setup */ - const io = socketio.listen(server, { - log: false, - }); + const io = new SocketIO({ path: path.join(urlPath, '/socket.io') }); + io.attach(server); if (doAuthorization) { io.use((socket, next) => { diff --git a/lib/connect_builder.js b/lib/connect_builder.js index b4bd33f..76754b3 100644 --- a/lib/connect_builder.js +++ b/lib/connect_builder.js @@ -3,14 +3,18 @@ const connect = require('connect'); const fs = require('fs'); -function ConnectBuilder() { +function ConnectBuilder(urlPath) { this.app = connect(); + this.urlPath = urlPath; } ConnectBuilder.prototype.authorize = function authorize(user, pass) { this.app.use( + this.urlPath, connect.basicAuth( - (incomingUser, incomingPass) => user === incomingUser && pass === incomingPass)); + (incomingUser, incomingPass) => user === incomingUser && pass === incomingPass + ) + ); return this; }; @@ -22,7 +26,7 @@ ConnectBuilder.prototype.build = function build() { ConnectBuilder.prototype.index = function index(path, files, filesNamespace, themeOpt) { const theme = themeOpt || 'default'; - this.app.use((req, res) => { + this.app.use(this.urlPath, (req, res) => { fs.readFile(path, (err, data) => { res.writeHead(200, { 'Content-Type': 'text/html', @@ -32,7 +36,8 @@ ConnectBuilder.prototype.index = function index(path, files, filesNamespace, the .toString('utf-8') .replace(/__TITLE__/g, files) .replace(/__THEME__/g, theme) - .replace(/__NAMESPACE__/g, filesNamespace), + .replace(/__NAMESPACE__/g, filesNamespace) + .replace(/__PATH__/g, this.urlPath), 'utf-8' ); }); @@ -42,8 +47,9 @@ ConnectBuilder.prototype.index = function index(path, files, filesNamespace, the }; ConnectBuilder.prototype.session = function session(secret, key) { - this.app.use(connect.cookieParser()); + this.app.use(this.urlPath, connect.cookieParser()); this.app.use( + this.urlPath, connect.session({ secret, key, @@ -53,8 +59,8 @@ ConnectBuilder.prototype.session = function session(secret, key) { }; ConnectBuilder.prototype.static = function staticf(path) { - this.app.use(connect.static(path)); + this.app.use(this.urlPath, connect.static(path)); return this; }; -module.exports = () => new ConnectBuilder(); +module.exports = urlPath => new ConnectBuilder(urlPath); diff --git a/lib/daemonize.js b/lib/daemonize.js index a63241b..dce1ec2 100644 --- a/lib/daemonize.js +++ b/lib/daemonize.js @@ -14,11 +14,16 @@ module.exports = (script, params, opts) => { const logFile = fs.openSync(params.logPath, 'a'); let args = [ - '-h', params.host, - '-p', params.port, - '-n', params.number, - '-l', params.lines, - '-t', params.theme, + '-h', + params.host, + '-p', + params.port, + '-n', + params.number, + '-l', + params.lines, + '-t', + params.theme, ]; if (options.doAuthorization) { @@ -33,6 +38,10 @@ module.exports = (script, params, opts) => { args.push('--ui-hide-topbar'); } + if (params.urlPath) { + args.push('--url-path', params.urlPath); + } + if (!params.uiIndent) { args.push('--ui-no-indent'); } diff --git a/lib/options_parser.js b/lib/options_parser.js index 4d3ab30..02d5267 100644 --- a/lib/options_parser.js +++ b/lib/options_parser.js @@ -45,6 +45,7 @@ program String, '/dev/null' ) + .option('--url-path ', 'URL path for the browser application, default /', String, '/') .option('--ui-hide-topbar', 'hide topbar (log file name and search box)') .option('--ui-no-indent', "don't indent log lines") .option( diff --git a/package-lock.json b/package-lock.json index 052f664..8f654d5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "frontail", - "version": "4.1.0", + "version": "4.1.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/test/connect_builder.js b/test/connect_builder.js index 9d0eed7..cfc5e33 100644 --- a/test/connect_builder.js +++ b/test/connect_builder.js @@ -6,7 +6,7 @@ const path = require('path'); describe('connectBuilder', () => { it('should build connect app', () => { - connectBuilder() + connectBuilder('/') .build() .should.have.property('use'); connectBuilder() @@ -15,7 +15,7 @@ describe('connectBuilder', () => { }); it('should build app requiring authorized user', (done) => { - const app = connectBuilder() + const app = connectBuilder('/') .authorize('user', 'pass') .build(); @@ -26,7 +26,7 @@ describe('connectBuilder', () => { }); it('should build app allowing user to login', (done) => { - const app = connectBuilder() + const app = connectBuilder('/') .authorize('user', 'pass') .build(); app.use((req, res) => { @@ -40,7 +40,7 @@ describe('connectBuilder', () => { }); it('should build app that setup session', (done) => { - const app = connectBuilder() + const app = connectBuilder('/') .session('secret', 'sessionkey') .build(); app.use((req, res) => { @@ -53,7 +53,7 @@ describe('connectBuilder', () => { }); it('should build app that serve static files', (done) => { - const app = connectBuilder() + const app = connectBuilder('/') .static(path.join(__dirname, 'fixtures')) .build(); @@ -63,7 +63,7 @@ describe('connectBuilder', () => { }); it('should build app that serve index file', (done) => { - const app = connectBuilder() + const app = connectBuilder('/') .index(path.join(__dirname, 'fixtures/index'), '/testfile') .build(); @@ -73,8 +73,19 @@ describe('connectBuilder', () => { .expect('Content-Type', 'text/html', done); }); + it('should build app that serve index file on specified path', (done) => { + const app = connectBuilder('/test') + .index(path.join(__dirname, 'fixtures/index'), '/testfile') + .build(); + + request(app) + .get('/test') + .expect(200) + .expect('Content-Type', 'text/html', done); + }); + it('should build app that replace index title', (done) => { - const app = connectBuilder() + const app = connectBuilder('/') .index(path.join(__dirname, 'fixtures/index_with_title'), '/testfile') .build(); @@ -84,7 +95,7 @@ describe('connectBuilder', () => { }); it('should build app that sets socket.io namespace based on files', (done) => { - const app = connectBuilder() + const app = connectBuilder('/') .index(path.join(__dirname, 'fixtures/index_with_ns'), '/testfile', 'ns', 'dark') .build(); @@ -94,7 +105,7 @@ describe('connectBuilder', () => { }); it('should build app that sets theme', (done) => { - const app = connectBuilder() + const app = connectBuilder('/') .index(path.join(__dirname, '/fixtures/index_with_theme'), '/testfile', 'ns', 'dark') .build(); @@ -107,7 +118,7 @@ describe('connectBuilder', () => { }); it('should build app that sets default theme', (done) => { - const app = connectBuilder() + const app = connectBuilder('/') .index(path.join(__dirname, '/fixtures/index_with_theme'), '/testfile') .build(); diff --git a/test/daemonize.js b/test/daemonize.js index 630da49..5aa84ee 100644 --- a/test/daemonize.js +++ b/test/daemonize.js @@ -107,6 +107,14 @@ describe('daemonize', () => { daemon.daemon.lastCall.args[1].should.containDeep(['-k', 'key.file', '-c', 'cert.file']); }); + it('with url-path option', () => { + optionsParser.parse(['node', '/path/to/frontail', '--url-path', '/test']); + + daemonize('script', optionsParser); + + daemon.daemon.lastCall.args[1].should.containDeep(['--url-path', '/test']); + }); + it('with hide-topbar option', () => { optionsParser.parse(['node', '/path/to/frontail', '--ui-hide-topbar']); diff --git a/web/index.html b/web/index.html index 2de868f..a62c018 100644 --- a/web/index.html +++ b/web/index.html @@ -1,11 +1,13 @@ + tail -F __TITLE__ - - + + +