diff --git a/belaUI.js b/belaUI.js index bb52bb2..1aef00e 100644 --- a/belaUI.js +++ b/belaUI.js @@ -155,6 +155,10 @@ wss.on('connection', function connection(conn) { console.log(`Error parsing client message: ${err.message}`); } }); + + conn.on('close', function close() { + removeLogListener(conn); + }); }); @@ -1912,6 +1916,134 @@ function tryAuth(conn, msg) { } +/* Log viewer */ +let logListeners = []; +const LOGSECRETREGEX = /<(\w+)>/gm; +const LOGSECRET = "****"; + +function spawnLogStream() { + const args = ['-o', 'json', '--all', '-fu', 'belaUI']; + const journalctl = spawn('journalctl', args); + + journalctl.stdout.on('data', (chunk) => { + if (logListeners.length == 0) { + journalctl.stdin.pause(); + journalctl.kill(); + + return; + } + + processLogStream(chunk.toString(), (msg) => { + let { MESSAGE, _COMM, __REALTIME_TIMESTAMP } = msg; + + for (c of logListeners) { + let json = { + comm: _COMM, + timestamp: +__REALTIME_TIMESTAMP, + message: hideSensitiveInfo(MESSAGE.replace(LOGSECRETREGEX, LOGSECRET)), + }; + + if (json.message.includes("keepalive")) continue; + + if (!c.logHideSensitive) { + const matches = [...MESSAGE.matchAll(LOGSECRETREGEX)]; + + if (matches.length > 0) { + matches.map((m) => (MESSAGE = MESSAGE.replace(m[0], m[1]))); + } + + json.message = MESSAGE; + } + + c.send(buildMsg('logStream', json)) + }; + }); + }); +} + +function hideSensitiveInfo(sentence) { + sentence = hideRegex(sentence, /"auth":{"\w+":"(.*?)"/gm) + sentence = hideWord(sentence, config.srt_streamid) + sentence = hideWord(sentence, config.srtla_addr) + sentence = hideWord(sentence, config.password_hash) + + return sentence; +} + +function hideWord(s, word) { + if (s.includes(word)) { + return s.replaceAll(word, LOGSECRET); + } + + return s; +} + +function hideRegex(s, regex) { + const matches = [...s.matchAll(regex)]; + + if (matches.length > 0) { + matches.map((m) => (s = s.replace(m[1], LOGSECRET))); + } + + return s; +} + +let partialJson = ''; +let prevPartial = false; +function processLogStream(msg, cb) { + if (prevPartial) { + msg = `${partialJson}${msg}`; + prevPartial = false; + partialJson = ''; + } + + for (const chunk of msg.trim().split('\n')) { + try { + cb(JSON.parse(chunk)); + } catch (error) { + partialJson += chunk; + prevPartial = true; + } + } +} + +function subscribeLogStream(conn, { show, hideSensitive }) { + if (!show) { + removeLogListener(conn); + return; + } + + if (logListeners.length == 0) spawnLogStream(); + + if (!logListeners.includes(conn)) { + conn.logHideSensitive = hideSensitive; + logListeners.push(conn); + } +} + +function removeLogListener(conn) { + logListeners = logListeners.filter((l) => l != conn); +} + +function downloadLog(conn, { hideSensitive }) { + const args = ['-u', 'belaUI', '-b']; + const journalctl = spawn('journalctl', args); + + journalctl.stdout.on('data', (chunk) => { + if (hideSensitive) { + const chunkString = hideSensitiveInfo(chunk.toString().replace(LOGSECRETREGEX, LOGSECRET)); + chunk = Buffer.from(chunkString); + } + + conn.send(buildMsg('logDownload', { chunk })); + }); + + journalctl.on('close', () => { + conn.send(buildMsg('logDownload', { completed: true })); + }); +} + + function handleMessage(conn, msg, isRemote = false) { console.log(msg); @@ -1974,6 +2106,12 @@ function handleMessage(conn, msg, isRemote = false) { delete conn.isAuthed; delete conn.authToken; + break; + case 'subscribeLogStream': + subscribeLogStream(conn, msg[type]); + break; + case 'downloadLog': + downloadLog(conn, msg[type]); break; } } diff --git a/public/index.html b/public/index.html index b25adb1..846dbe2 100644 --- a/public/index.html +++ b/public/index.html @@ -176,6 +176,14 @@
+
+
+
+ +
+
Connect to a new network
+
+
+ +
+ +
+
+
+ + +
+ + +
+
+
+
diff --git a/public/script.js b/public/script.js index 3ba66df..cf7400d 100644 --- a/public/script.js +++ b/public/script.js @@ -41,6 +41,8 @@ function tryConnect() { hideError(); tryTokenAuth(); updateNetact(true); + + if (logStream) sendLogStreamRequest(); }); } @@ -768,6 +770,57 @@ function handleWifiResult(msg) { } +/* Logs */ +const MAX_LOGS = 100; +const logs = document.getElementById('logs'); + +function handleLogstream(log) { + const { comm, timestamp, message } = log; + const date = new Date(timestamp / 1000); + + const html = `
+
+

${comm}

+

${date.toLocaleString()}

+
+
+

${message}

+
+
`; + + if (logs.childElementCount == MAX_LOGS) logs.lastElementChild.remove(); + + logs.insertAdjacentHTML('afterbegin', html); +} + +let chunkBlobs = []; +function handleLogDownload(log) { + if (log.completed) { + console.log('Download completed'); + finalBlob = new Blob(chunkBlobs, { type: 'text/plain' }); + saveToFile(finalBlob, `BELABOX_${new Date().toJSON()}.txt`); + chunkBlobs = []; + + return; + } + + console.log('Downloading log'); + + const chunk = new Uint8Array(log.chunk.data); + const chunkBlob = new Blob([chunk], { type: 'text/plain' }); + chunkBlobs.push(chunkBlob); +} + +function saveToFile(blob, name) { + const a = document.createElement('a'); + a.href = URL.createObjectURL(blob); + a.download = name; + a.click(); + a.remove(); + URL.revokeObjectURL(a.href); +} + + /* Error messages */ function showError(message) { $("#errorMsg>span").text(message); @@ -893,6 +946,12 @@ function handleMessage(msg) { case 'wifi': handleWifiResult(msg[type]); break; + case 'logStream': + handleLogstream(msg[type]); + break; + case 'logDownload': + handleLogDownload(msg[type]); + break; case 'error': showError(msg[type].msg); break; @@ -1201,3 +1260,35 @@ $('input.click-copy').click(function(ev) { }, 3000); } }); + +let logStream = false; +const logStreamEle = document.querySelectorAll('.logStream'); +const hideSensitive = document.getElementById('logHideSensitive'); + +logStreamEle.forEach(l => l.addEventListener('click', toggleLog)); + +function toggleLog() { + logStream = !logStream; + const hideShow = logStream ? 'Hide' : 'Show'; + logs.innerHTML = ''; + + sendLogStreamRequest(); + + const logTable = document.getElementById('logTable'); + logTable.classList.toggle('d-none'); + + logStreamEle.forEach((i) => (i.innerText = `${hideShow} current log`)); +} + +function sendLogStreamRequest() { + ws.send(JSON.stringify({ + subscribeLogStream: { + show: logStream, + hideSensitive: hideSensitive.checked + } + })); +} + +document.getElementById('logDownload').addEventListener('click', () => { + ws.send(JSON.stringify({ downloadLog: { hideSensitive: hideSensitive.checked } })); +});